diff --git a/apps/mobile/app/app.tsx b/apps/mobile/app/app.tsx index 9a6586fe0..091d618c7 100644 --- a/apps/mobile/app/app.tsx +++ b/apps/mobile/app/app.tsx @@ -19,20 +19,22 @@ along with this program. If not, see . import { i18n } from "@lingui/core"; import { I18nProvider } from "@lingui/react"; import { + ScopedThemeProvider, THEME_COMPATIBILITY_VERSION, useThemeEngineStore } from "@notesnook/theme"; import React, { useEffect } from "react"; -import { I18nManager, View } from "react-native"; +import { I18nManager, StatusBar } from "react-native"; import "react-native-gesture-handler"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { SafeAreaProvider } from "react-native-safe-area-context"; -import AppLockedOverlay from "./components/app-lock-overlay"; +import DialogProvider from "./components/dialog-provider"; import { withErrorBoundry } from "./components/exception-handler"; import GlobalSafeAreaProvider from "./components/globalsafearea"; +import { Toast } from "./components/toast"; import { useAppEvents } from "./hooks/use-app-events"; -import { ApplicationHolder } from "./navigation"; import { NotePreviewConfigure } from "./screens/note-preview-configure"; +import { RootNavigation } from "./navigation/navigation-stack"; import { themeTrpcClient } from "./screens/settings/theme-selector"; import Notifications from "./services/notifications"; import SettingsService from "./services/settings"; @@ -64,42 +66,26 @@ const App = (props: { configureMode: "note-preview" }) => { }, []); return ( - - - - - - - + + + {props.configureMode === "note-preview" ? ( ) : ( - + )} + + + + - - + ); }; diff --git a/apps/mobile/app/components/app-lock-overlay/index.tsx b/apps/mobile/app/components/app-lock-overlay/index.tsx index 9f13e012e..f376778b5 100644 --- a/apps/mobile/app/components/app-lock-overlay/index.tsx +++ b/apps/mobile/app/components/app-lock-overlay/index.tsx @@ -82,7 +82,7 @@ const verifyUserPassword = async (password: string) => { } }; -const AppLockedOverlay = () => { +const AppLockedScreen = () => { const initialLaunchBiometricRequest = useRef(true); const { colors } = useThemeColors(); const user = getUser(); @@ -203,14 +203,12 @@ const AppLockedOverlay = () => { } }, [appState, onUnlockAppRequested, appLocked]); - return appLocked ? ( + return ( { - ) : null; + ); }; -export default AppLockedOverlay; +export default AppLockedScreen; diff --git a/apps/mobile/app/components/container/floating-button.tsx b/apps/mobile/app/components/container/floating-button.tsx index 22146e3e7..6cb76f7b1 100644 --- a/apps/mobile/app/components/container/floating-button.tsx +++ b/apps/mobile/app/components/container/floating-button.tsx @@ -20,7 +20,7 @@ along with this program. If not, see . import { useThemeColors } from "@notesnook/theme"; import { useRoute } from "@react-navigation/native"; import React, { useCallback, useEffect } from "react"; -import { Keyboard, View } from "react-native"; +import { Keyboard, TouchableOpacity, View } from "react-native"; import Animated, { Easing, useAnimatedStyle, @@ -34,7 +34,7 @@ import { useSelectionStore } from "../../stores/use-selection-store"; import { useSettingStore } from "../../stores/use-setting-store"; import { getElevationStyle } from "../../utils/elevation"; import { SIZE, normalize } from "../../utils/size"; -import { Pressable } from "../ui/pressable"; +import { DefaultAppStyles } from "../../utils/styles"; interface FloatingButtonProps { onPress: () => void; @@ -108,20 +108,24 @@ const FloatingButton = ({ style={[ { position: "absolute", - right: 12, + right: DefaultAppStyles.GAP, bottom: 20, zIndex: 10 }, animatedStyle ]} > - @@ -130,7 +134,9 @@ const FloatingButton = ({ alignItems: "center", justifyContent: "center", height: normalize(60), - width: normalize(60) + width: normalize(60), + backgroundColor: colors.primary.shade, + borderRadius: 20 }} > - + ); }; diff --git a/apps/mobile/app/components/dialog/dialog-buttons.js b/apps/mobile/app/components/dialog/dialog-buttons.js index ca1410f4c..f4a3d8f23 100644 --- a/apps/mobile/app/components/dialog/dialog-buttons.js +++ b/apps/mobile/app/components/dialog/dialog-buttons.js @@ -46,8 +46,6 @@ const DialogButtons = ({ { backgroundColor: colors.secondary.background, height: 60, - borderBottomRightRadius: 5, - borderBottomLeftRadius: 5, paddingHorizontal: 12, borderTopWidth: 0.7, borderTopColor: getColorLinearShade( diff --git a/apps/mobile/app/components/dialog/index.js b/apps/mobile/app/components/dialog/index.js index 1549f93d6..04b035f84 100644 --- a/apps/mobile/app/components/dialog/index.js +++ b/apps/mobile/app/components/dialog/index.js @@ -39,6 +39,7 @@ import { Button } from "../ui/button"; import { getContainerBorder } from "../../utils/colors"; import { Notice } from "../ui/notice"; import { strings } from "@notesnook/intl"; +import { br } from "../../utils/size"; export const Dialog = ({ context = "global" }) => { const { colors } = useThemeColors(); @@ -125,10 +126,11 @@ export const Dialog = ({ context = "global" }) => { ...getElevationStyle(5), width: DDS.isTab ? 400 : "85%", maxHeight: 450, - borderRadius: 5, + borderRadius: br, backgroundColor: colors.primary.background, paddingTop: 12, - ...getContainerBorder(colors.primary.border, 0.5) + ...getContainerBorder(colors.primary.border, 0.5), + overflow: "hidden" }; return visible ? ( diff --git a/apps/mobile/app/components/dialogs/vault/index.js b/apps/mobile/app/components/dialogs/vault/index.js index e2d0214d4..c192cc7a4 100644 --- a/apps/mobile/app/components/dialogs/vault/index.js +++ b/apps/mobile/app/components/dialogs/vault/index.js @@ -41,7 +41,7 @@ import { eUpdateNoteInEditor } from "../../../utils/events"; import { deleteItems } from "../../../utils/functions"; -import { tabBarRef } from "../../../utils/global-refs"; +import { fluidTabsRef } from "../../../utils/global-refs"; import { convertNoteToText } from "../../../utils/note-to-text"; import { sleep } from "../../../utils/time"; import BaseDialog from "../../dialog/base-dialog"; @@ -553,7 +553,7 @@ export class VaultDialog extends Component { item: note }); if (!DDS.isTab) { - tabBarRef.current?.goToPage(1); + fluidTabsRef.current?.goToPage(1); } }); } diff --git a/apps/mobile/app/components/tabs/index.tsx b/apps/mobile/app/components/fluid-panels/index.tsx similarity index 93% rename from apps/mobile/app/components/tabs/index.tsx rename to apps/mobile/app/components/fluid-panels/index.tsx index 11d58c90b..e4c8298e1 100644 --- a/apps/mobile/app/components/tabs/index.tsx +++ b/apps/mobile/app/components/fluid-panels/index.tsx @@ -37,12 +37,12 @@ import Animated, { WithSpringConfig, withTiming } from "react-native-reanimated"; +import { useTabStore } from "../../screens/editor/tiptap/use-tab-store"; import { getAppState } from "../../screens/editor/tiptap/utils"; import { eSendEvent } from "../../services/event-manager"; import { useSettingStore } from "../../stores/use-setting-store"; import { eClearEditor } from "../../utils/events"; import { useSideBarDraggingStore } from "../side-menu/dragging-store"; -import { useTabStore } from "../../screens/editor/tiptap/use-tab-store"; interface TabProps extends ViewProps { dimensions: { width: number; height: number }; @@ -66,7 +66,7 @@ export interface TabsRef { node: RefObject; } -export const FluidTabs = forwardRef(function FluidTabs( +export const FluidPanels = forwardRef(function FluidTabs( { children, dimensions, @@ -81,9 +81,6 @@ export const FluidTabs = forwardRef(function FluidTabs( const appState = useMemo(() => getAppState(), []); const deviceMode = useSettingStore((state) => state.deviceMode); const fullscreen = useSettingStore((state) => state.fullscreen); - const introCompleted = useSettingStore( - (state) => state.settings.introCompleted - ); const translateX = useSharedValue( widths ? appState && @@ -118,26 +115,34 @@ export const FluidTabs = forwardRef(function FluidTabs( const isIPhone = Platform.OS === "ios"; useEffect(() => { - if (introCompleted) { - if (deviceMode === "tablet" || fullscreen) { - translateX.value = 0; - } else { - if (prevWidths.current?.sidebar !== widths.sidebar) { - translateX.value = - appState && appState?.movedAway === false - ? editorPosition - : widths.sidebar; - if (translateX.value === editorPosition) { - onChangeTab?.({ i: 2, from: 1 }); - } + if (deviceMode === "tablet" || fullscreen) { + translateX.value = 0; + } else { + if (prevWidths.current?.sidebar !== widths.sidebar) { + translateX.value = + appState && appState?.movedAway === false + ? editorPosition + : widths.sidebar; + if (translateX.value === editorPosition) { + onChangeTab?.({ i: 2, from: 1 }); } } - isLoaded.current = true; - prevWidths.current = widths; } + isLoaded.current = true; + prevWidths.current = widths; + }, [ + deviceMode, + widths, + fullscreen, + translateX, + editorPosition, + appState, + onChangeTab + ]); + useEffect(() => { const sub = BackHandler.addEventListener("hardwareBackPress", () => { - if (isDrawerOpen.value) { + if (isDrawerOpen.value && !forcedLock.value) { translateX.value = withTiming(homePosition); onDrawerStateChange(false); isDrawerOpen.value = false; @@ -149,16 +154,11 @@ export const FluidTabs = forwardRef(function FluidTabs( sub && sub.remove(); }; }, [ - introCompleted, - deviceMode, - widths, - fullscreen, - translateX, - isDrawerOpen, + forcedLock.value, homePosition, + isDrawerOpen, onDrawerStateChange, - editorPosition, - appState + translateX ]); useImperativeHandle( @@ -294,8 +294,8 @@ export const FluidTabs = forwardRef(function FluidTabs( const gesture = Gesture.Pan() .maxPointers(1) .enabled(enabled && !disabled) - .activeOffsetX([-5, 5]) - .failOffsetY([-15, 15]) + .activeOffsetX([-20, 20]) + .failOffsetY([-10, 10]) .onBegin((event) => { locked.value = false; gestureStartValue.value = { diff --git a/apps/mobile/app/components/header/index.tsx b/apps/mobile/app/components/header/index.tsx index 2f94db30a..c55443ea3 100644 --- a/apps/mobile/app/components/header/index.tsx +++ b/apps/mobile/app/components/header/index.tsx @@ -17,60 +17,52 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +import { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; import React, { useCallback, useEffect, useState } from "react"; -import { Platform, StyleSheet, View } from "react-native"; -import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; +import { View } from "react-native"; import { eSubscribeEvent, eUnSubscribeEvent } from "../../services/event-manager"; -import useNavigationStore, { - RouteName -} from "../../stores/use-navigation-store"; +import { RouteName } from "../../stores/use-navigation-store"; import { useSelectionStore } from "../../stores/use-selection-store"; import { eScrollEvent } from "../../utils/events"; +import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; +import { IconButtonProps } from "../ui/icon-button"; +import { Pressable } from "../ui/pressable"; +import Heading from "../ui/typography/heading"; +import Paragraph from "../ui/typography/paragraph"; import { LeftMenus } from "./left-menus"; import { RightMenus } from "./right-menus"; -import { Title } from "./title"; - -type HeaderRightButton = { - title: string; - onPress: () => void; -}; export const Header = ({ renderedInRoute, onLeftMenuButtonPress, title, - titleHiddenOnRender, - headerRightButtons, id, - accentColor, - isBeta, canGoBack, - onPressDefaultRightButton, hasSearch, - onSearch + onSearch, + rightButton }: { onLeftMenuButtonPress?: () => void; renderedInRoute?: RouteName; id?: string; title: string; - headerRightButtons?: HeaderRightButton[]; - titleHiddenOnRender?: boolean; - accentColor?: string; - isBeta?: boolean; canGoBack?: boolean; onPressDefaultRightButton?: () => void; hasSearch?: boolean; onSearch?: () => void; + rightButton?: IconButtonProps; }) => { const { colors } = useThemeColors(); - const insets = useGlobalSafeAreaInsets(); const [borderHidden, setBorderHidden] = useState(true); - const selectionMode = useSelectionStore((state) => state.selectionMode); - const isFocused = useNavigationStore((state) => state.focusedRouteId === id); + const [selectedItemsList, selectionMode] = useSelectionStore((state) => [ + state.selectedItemsList, + state.selectionMode + ]); const onScroll = useCallback( (data: { x: number; y: number; id?: string; route: string }) => { @@ -93,88 +85,47 @@ export const Header = ({ }; }, [borderHidden, onScroll]); - return selectionMode && isFocused ? null : ( - <> - - <> - - + const HeaderWrapper = hasSearch ? Pressable : View; - - </View> - <RightMenus - renderedInRoute={renderedInRoute} - id={id} - headerRightButtons={headerRightButtons} - onPressDefaultRightButton={onPressDefaultRightButton} - search={hasSearch} - onSearch={onSearch} - /> - </> - </View> - </> + return ( + <View + style={{ + paddingHorizontal: DefaultAppStyles.GAP + }} + > + <HeaderWrapper + style={{ + flexDirection: "row", + justifyContent: "space-between", + marginTop: DefaultAppStyles.GAP_SMALL, + borderRadius: 10, + paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL, + borderWidth: hasSearch ? 1 : 0, + borderColor: colors.secondary.border, + paddingHorizontal: !hasSearch ? 0 : DefaultAppStyles.GAP_SMALL, + alignItems: "center" + }} + onPress={() => { + onSearch?.(); + }} + > + <LeftMenus + canGoBack={canGoBack} + onLeftButtonPress={onLeftMenuButtonPress} + /> + + {hasSearch ? ( + <Paragraph> + {selectionMode + ? `${selectedItemsList.length} selected` + : strings.searchInRoute(title)} + </Paragraph> + ) : ( + <Heading size={SIZE.lg}>{title}</Heading> + )} + + <RightMenus rightButton={rightButton} /> + </HeaderWrapper> + </View> ); }; - -const styles = StyleSheet.create({ - container: { - flexDirection: "row", - zIndex: 11, - height: 50, - maxHeight: 50, - justifyContent: "center", - alignItems: "center", - paddingHorizontal: 12, - width: "100%" - }, - leftBtnContainer: { - flexDirection: "row", - justifyContent: "flex-start", - alignItems: "center", - flexShrink: 1 - }, - leftBtn: { - justifyContent: "center", - alignItems: "center", - height: 40, - width: 40, - borderRadius: 100, - marginLeft: -5, - marginRight: 25 - }, - rightBtnContainer: { - flexDirection: "row", - alignItems: "center" - }, - rightBtn: { - justifyContent: "center", - alignItems: "flex-end", - height: 40, - width: 40, - paddingRight: 0 - } -}); diff --git a/apps/mobile/app/components/header/left-menus.tsx b/apps/mobile/app/components/header/left-menus.tsx index e6aa0fcaf..c95530cfe 100644 --- a/apps/mobile/app/components/header/left-menus.tsx +++ b/apps/mobile/app/components/header/left-menus.tsx @@ -20,10 +20,9 @@ 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 { useSettingStore } from "../../stores/use-setting-store"; -import { tabBarRef } from "../../utils/global-refs"; +import { fluidTabsRef } from "../../utils/global-refs"; import { IconButton } from "../ui/icon-button"; export const LeftMenus = ({ @@ -41,7 +40,7 @@ export const LeftMenus = ({ if (onLeftButtonPress) return onLeftButtonPress(); if (!canGoBack) { - if (tabBarRef.current?.isDrawerOpen()) { + if (fluidTabsRef.current?.isDrawerOpen()) { Navigation.closeDrawer(); } else { Navigation.openDrawer(); @@ -54,23 +53,14 @@ export const LeftMenus = ({ return isTablet && !canGoBack ? null : ( <IconButton testID={notesnook.ids.default.header.buttons.left} - style={{ - justifyContent: "center", - alignItems: "center", - height: 40, - width: 40, - borderRadius: 100, - marginRight: DDS.isLargeTablet() ? 10 : 7 - }} left={40} top={40} - right={DDS.isLargeTablet() ? 10 : 10} onPress={_onLeftButtonPress} onLongPress={() => { Navigation.popToTop(); }} name={canGoBack ? "arrow-left" : "menu"} - color={colors.primary.paragraph} + color={colors.primary.icon} /> ); }; diff --git a/apps/mobile/app/components/header/right-menus.tsx b/apps/mobile/app/components/header/right-menus.tsx index 8f4ec9f0b..fe44ed5ca 100644 --- a/apps/mobile/app/components/header/right-menus.tsx +++ b/apps/mobile/app/components/header/right-menus.tsx @@ -17,127 +17,30 @@ 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 } from "react"; -import { Platform, StyleSheet, View } from "react-native"; import { useThemeColors } from "@notesnook/theme"; -import { Menu } from "react-native-material-menu"; -import { notesnook } from "../../../e2e/test.ids"; -import { - HeaderRightButton, - RouteName -} from "../../stores/use-navigation-store"; -import { useSettingStore } from "../../stores/use-setting-store"; -import { SIZE } from "../../utils/size"; -import { sleep } from "../../utils/time"; -import { Button } from "../ui/button"; -import { IconButton } from "../ui/icon-button"; +import React from "react"; +import { StyleSheet, View } from "react-native"; +import { IconButton, IconButtonProps } from "../ui/icon-button"; export const RightMenus = ({ - headerRightButtons, - renderedInRoute, - id, - onPressDefaultRightButton, - search, - onSearch + rightButton }: { - headerRightButtons?: HeaderRightButton[]; - renderedInRoute?: RouteName; - id?: string; - onPressDefaultRightButton?: () => void; - search?: boolean; - onSearch?: () => void; + rightButton?: IconButtonProps; }) => { const { colors } = useThemeColors(); - const { colors: contextMenuColors } = useThemeColors("contextMenu"); - const deviceMode = useSettingStore((state) => state.deviceMode); - const menuRef = useRef<Menu>(null); return ( <View style={styles.rightBtnContainer}> - {search ? ( - <IconButton - onPress={onSearch} - testID="icon-search" - name="magnify" - color={colors.primary.paragraph} - style={styles.rightBtn} - /> - ) : null} - - {deviceMode !== "mobile" ? ( - <Button - onPress={onPressDefaultRightButton} - testID={notesnook.ids.default.addBtn} - icon={renderedInRoute === "Trash" ? "delete" : "plus"} - iconSize={SIZE.xl} - type="accent" - hitSlop={{ - top: 10, - right: 10, - bottom: 10, - left: 0 - }} + {rightButton ? ( + <IconButton {...rightButton} color={colors.primary.icon} /> + ) : ( + <View style={{ - marginLeft: 10, - width: 32, - height: 32, - borderRadius: 100, - paddingHorizontal: 0, - borderWidth: 1, - borderColor: colors.primary.accent + width: 40, + height: 40 }} /> - ) : null} - - {headerRightButtons && headerRightButtons.length > 0 ? ( - <Menu - ref={menuRef} - animationDuration={200} - style={{ - borderRadius: 5, - backgroundColor: contextMenuColors.primary.background, - marginTop: 35 - }} - onRequestClose={() => { - //@ts-ignore - menuRef.current?.hide(); - }} - anchor={ - <IconButton - onPress={() => { - //@ts-ignore - menuRef.current?.show(); - }} - name="dots-vertical" - color={colors.primary.paragraph} - style={styles.rightBtn} - /> - } - > - {headerRightButtons.map((item) => ( - <Button - style={{ - justifyContent: "flex-start", - borderRadius: 0, - alignSelf: "flex-start", - width: "100%" - }} - type="plain" - buttonType={{ - text: contextMenuColors.primary.paragraph - }} - key={item.title} - title={item.title} - onPress={async () => { - //@ts-ignore - menuRef.current?.hide(); - if (Platform.OS === "ios") await sleep(300); - item.onPress(); - }} - /> - ))} - </Menu> - ) : null} + )} </View> ); }; @@ -149,10 +52,6 @@ const styles = StyleSheet.create({ }, rightBtn: { justifyContent: "center", - alignItems: "center", - height: 40, - width: 40, - marginLeft: 10, - paddingRight: 0 + alignItems: "center" } }); diff --git a/apps/mobile/app/components/list-items/headers/notebook-header.tsx b/apps/mobile/app/components/list-items/headers/notebook-header.tsx index 8d0f154f6..8f62c336b 100644 --- a/apps/mobile/app/components/list-items/headers/notebook-header.tsx +++ b/apps/mobile/app/components/list-items/headers/notebook-header.tsx @@ -17,140 +17,65 @@ 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 { getFormattedDate } from "@notesnook/common"; import { Notebook } from "@notesnook/core"; +import { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; -import React, { useState } from "react"; +import React from "react"; import { View } from "react-native"; -import { db } from "../../../common/database"; -import { ToastManager } from "../../../services/event-manager"; -import { useMenuStore } from "../../../stores/use-menu-store"; import { SIZE } from "../../../utils/size"; -import { IconButton } from "../../ui/icon-button"; +import { DefaultAppStyles } from "../../../utils/styles"; +import AppIcon from "../../ui/AppIcon"; import Heading from "../../ui/typography/heading"; import Paragraph from "../../ui/typography/paragraph"; -import { getFormattedDate } from "@notesnook/common"; -import { strings } from "@notesnook/intl"; export const NotebookHeader = ({ notebook, - onEditNotebook, totalNotes = 0 }: { notebook: Notebook; - onEditNotebook: () => void; totalNotes: number; }) => { const { colors } = useThemeColors(); - const [isPinnedToMenu, setIsPinnedToMenu] = useState( - db.shortcuts.exists(notebook.id) - ); - const setMenuPins = useMenuStore((state) => state.setMenuPins); - - const onPinNotebook = async () => { - try { - if (isPinnedToMenu) { - await db.shortcuts.remove(notebook.id); - } else { - await db.shortcuts.add({ - item: { - id: notebook.id, - type: "notebook" - } - }); - ToastManager.show({ - heading: strings.shortcutCreated(), - type: "success" - }); - } - setIsPinnedToMenu(db.shortcuts.exists(notebook.id)); - setMenuPins(); - } catch (e) { - console.error(e); - } - }; - return ( <View style={{ - marginBottom: 5, - padding: 0, - width: "100%", - paddingVertical: 15, - paddingHorizontal: 12, - alignSelf: "center", - borderRadius: 10, - paddingTop: 25 + paddingHorizontal: DefaultAppStyles.GAP, + marginVertical: DefaultAppStyles.GAP, + backgroundColor: colors.secondary.background }} > - <Paragraph color={colors.secondary.paragraph} size={SIZE.xs}> - {getFormattedDate(notebook.dateModified, "date-time")} - </Paragraph> <View style={{ - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center" + width: "100%", + gap: DefaultAppStyles.GAP_VERTICAL, + paddingVertical: 25 }} > - <Heading - style={{ - flexShrink: 1 - }} - size={SIZE.lg} - > - {notebook.title} - </Heading> + <AppIcon name="notebook" size={SIZE.xxl} /> + <Heading size={SIZE.lg}>{notebook.title}</Heading> + + {notebook.description ? ( + <Paragraph size={SIZE.sm} color={colors.primary.paragraph}> + {notebook.description} + </Paragraph> + ) : null} <View style={{ - flexDirection: "row" + flexDirection: "row", + gap: DefaultAppStyles.GAP_SMALL }} > - <IconButton - name={isPinnedToMenu ? "link-variant-off" : "link-variant"} - onPress={onPinNotebook} - tooltipText={"Create shortcut in side menu"} - style={{ - marginRight: 15, - width: 40, - height: 40 - }} - type="transparent" - color={isPinnedToMenu ? colors.primary.accent : colors.primary.icon} - size={SIZE.lg} - /> - <IconButton - size={SIZE.lg} - onPress={onEditNotebook} - tooltipText="Edit this notebook" - name="pencil" - type="transparent" - color={colors.primary.icon} - style={{ - width: 40, - height: 40 - }} - /> + <Paragraph size={SIZE.xxs} color={colors.secondary.paragraph}> + {strings.notes(totalNotes || 0)} + </Paragraph> + + <Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}> + {getFormattedDate(notebook.dateModified, "date-time")} + </Paragraph> </View> </View> - - {notebook.description ? ( - <Paragraph size={SIZE.sm} color={colors.primary.paragraph}> - {notebook.description} - </Paragraph> - ) : null} - - <Paragraph - style={{ - marginTop: 10, - fontStyle: "italic", - fontFamily: undefined - }} - size={SIZE.xs} - color={colors.secondary.paragraph} - > - {strings.notes(totalNotes || 0)} - </Paragraph> </View> ); }; 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 559b97441..f82e17aab 100644 --- a/apps/mobile/app/components/list-items/headers/section-header.tsx +++ b/apps/mobile/app/components/list-items/headers/section-header.tsx @@ -21,16 +21,16 @@ import { GroupHeader, GroupOptions, ItemType } from "@notesnook/core"; import { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; import React from "react"; -import { TouchableOpacity, View, useWindowDimensions } from "react-native"; +import { useWindowDimensions, View } from "react-native"; import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled"; import { presentSheet } from "../../../services/event-manager"; import SettingsService from "../../../services/settings"; import { RouteName } from "../../../stores/use-navigation-store"; -import { getContainerBorder } from "../../../utils/colors"; import { SIZE } from "../../../utils/size"; +import { DefaultAppStyles } from "../../../utils/styles"; import Sort from "../../sheets/sort"; -import { Button } from "../../ui/button"; import { IconButton } from "../../ui/icon-button"; +import { Pressable } from "../../ui/pressable"; import Heading from "../../ui/typography/heading"; type SectionHeaderProps = { @@ -65,104 +65,111 @@ export const SectionHeader = React.memo< return ( <View style={{ - flexDirection: "row", - alignItems: "center", - width: "95%", - justifyContent: "space-between", - paddingHorizontal: 12, - height: 35 * fontScale, - backgroundColor: colors.secondary.background, - alignSelf: "center", - borderRadius: 5, - marginVertical: 5, - ...getContainerBorder(colors.secondary.background, 0.8) + width: "100%", + paddingHorizontal: DefaultAppStyles.GAP, + marginVertical: DefaultAppStyles.GAP }} > - <TouchableOpacity - onPress={() => { - onOpenJumpToDialog(); - }} - activeOpacity={0.9} - hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }} - style={{ - height: "100%", - justifyContent: "center" - }} - > - <Heading - color={color || colors.primary.accent} - size={SIZE.sm} - style={{ - alignSelf: "center", - textAlignVertical: "center" - }} - > - {!item.title || item.title === "" ? strings.pinned() : item.title} - </Heading> - </TouchableOpacity> - <View style={{ flexDirection: "row", - alignItems: "center" + alignItems: "center", + width: "100%", + alignSelf: "center", + justifyContent: "space-between", + borderBottomWidth: 1, + borderColor: colors.primary.border, + paddingBottom: DefaultAppStyles.GAP_VERTICAL }} > - {index === 0 ? ( - <> - <Button - onPress={() => { - presentSheet({ - component: <Sort screen={screen} type={dataType} /> - }); - }} - title={groupBy} - icon={ - groupOptions.sortDirection === "asc" - ? "sort-ascending" - : "sort-descending" - } - height={25} - style={{ - borderRadius: 100, - paddingHorizontal: 0, - backgroundColor: "transparent", - marginRight: - dataType === "note" || - screen === "Notes" || - dataType === "notebook" - ? 10 - : 0 - }} - type="plain" - iconPosition="right" - /> + <Pressable + onPress={() => { + onOpenJumpToDialog(); + }} + hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }} + style={{ + justifyContent: "flex-start", + flexDirection: "row", + width: "auto" + }} + > + <Heading + size={SIZE.sm} + style={{ + alignSelf: "center", + textAlignVertical: "center" + }} + color={colors.primary.accent} + > + {!item.title || item.title === "" ? strings.pinned() : item.title} + </Heading> + </Pressable> - <IconButton - style={{ - width: 25, - height: 25 - }} - hidden={ - dataType !== "note" && - dataType !== "notebook" && - screen !== "Notes" - } - testID="icon-compact-mode" - color={colors.secondary.icon} - name={isCompactModeEnabled ? "view-list" : "view-list-outline"} - onPress={() => { - SettingsService.set({ - [dataType !== "notebook" - ? "notesListMode" - : "notebooksListMode"]: !isCompactModeEnabled - ? "compact" - : "normal" - }); - }} - size={SIZE.lg - 2} - /> - </> - ) : null} + <View + style={{ + flexDirection: "row", + alignItems: "center", + gap: DefaultAppStyles.GAP_SMALL + }} + > + {index === 0 ? ( + <> + <IconButton + name={ + groupOptions.sortDirection === "asc" + ? "sort-ascending" + : "sort-descending" + } + onPress={() => { + if (!screen) return; + presentSheet({ + component: <Sort screen={screen} type={dataType} /> + }); + }} + style={{ + width: 25, + height: 25 + }} + size={SIZE.lg - 2} + /> + <IconButton + hidden={ + dataType !== "note" && + dataType !== "notebook" && + screen !== "Notes" + } + style={{ + width: 25, + height: 25 + }} + testID="icon-compact-mode" + color={colors.secondary.icon} + name={ + isCompactModeEnabled ? "view-list" : "view-list-outline" + } + onPress={() => { + SettingsService.set({ + [dataType !== "notebook" + ? "notesListMode" + : "notebooksListMode"]: !isCompactModeEnabled + ? "compact" + : "normal" + }); + }} + size={SIZE.lg - 2} + /> + </> + ) : null} + + <IconButton + style={{ + width: 25, + height: 25 + }} + name={"chevron-down"} + size={SIZE.lg - 2} + /> + </View> </View> </View> ); diff --git a/apps/mobile/app/components/list-items/note/index.tsx b/apps/mobile/app/components/list-items/note/index.tsx index 60cb08205..006ae8343 100644 --- a/apps/mobile/app/components/list-items/note/index.tsx +++ b/apps/mobile/app/components/list-items/note/index.tsx @@ -29,24 +29,25 @@ import { EntityLevel, decode } from "entities"; import React from "react"; import { View } from "react-native"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; -import { notesnook } from "../../../../e2e/test.ids"; import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled"; -import NotebookScreen from "../../../screens/notebook"; -import { TaggedNotes } from "../../../screens/notes/tagged"; import useNavigationStore from "../../../stores/use-navigation-store"; import { useRelationStore } from "../../../stores/use-relation-store"; import { SIZE } from "../../../utils/size"; -import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store"; import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common"; +import { strings } from "@notesnook/intl"; +import { notesnook } from "../../../../e2e/test.ids"; +import useIsSelected from "../../../hooks/use-selected"; +import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store"; +import { useSelectionStore } from "../../../stores/use-selection-store"; +import { DefaultAppStyles } from "../../../utils/styles"; import { Properties } from "../../properties"; -import { Button } from "../../ui/button"; +import AppIcon from "../../ui/AppIcon"; import { IconButton } from "../../ui/icon-button"; import { ReminderTime } from "../../ui/reminder-time"; import { TimeSince } from "../../ui/time-since"; import Heading from "../../ui/typography/heading"; import Paragraph from "../../ui/typography/paragraph"; -import { strings } from "@notesnook/intl"; type NoteItemProps = { item: Note | BaseTrashItem<Note>; @@ -85,6 +86,8 @@ const NoteItem = ({ ); const _update = useRelationStore((state) => state.updater); const primaryColors = isEditingNote ? colors.selected : colors.primary; + const selectionMode = useSelectionStore((state) => state.selectionMode); + const [selected] = useIsSelected(item); return ( <> @@ -94,64 +97,6 @@ const NoteItem = ({ flexShrink: 1 }} > - {!compactMode ? ( - <View - style={{ - flexDirection: "row", - alignItems: "center", - zIndex: 10, - elevation: 10, - marginBottom: 2.5, - flexWrap: "wrap" - }} - > - {notebooks?.items - ?.filter( - (item) => - item.id !== useNavigationStore.getState().focusedRouteId - ) - .map((item) => ( - <Button - title={ - item.title.length > 25 - ? item.title.slice(0, 25) + "..." - : item.title - } - tooltipText={item.title} - key={item.id} - height={25} - icon="book-outline" - type="secondary" - fontSize={SIZE.xs} - iconSize={SIZE.sm} - textStyle={{ - marginRight: 0 - }} - style={{ - borderRadius: 5, - marginRight: 5, - paddingHorizontal: 6, - marginBottom: 5 - }} - onPress={() => { - NotebookScreen.navigate(item, true); - }} - /> - ))} - - <ReminderTime - reminder={reminder} - color={color?.colorCode} - onPress={() => { - Properties.present(reminder); - }} - style={{ - height: 25 - }} - /> - </View> - ) : null} - {compactMode ? ( <Paragraph numberOfLines={1} @@ -167,7 +112,7 @@ const NoteItem = ({ <Heading numberOfLines={1} color={color?.colorCode || primaryColors.heading} - size={SIZE.md} + size={SIZE.sm} style={{ paddingRight: 10 }} @@ -198,7 +143,9 @@ const NoteItem = ({ alignItems: "center", width: "100%", marginTop: 5, - height: SIZE.md + 2 + columnGap: 8, + rowGap: 4, + flexWrap: "wrap" }} > {!isTrash ? ( @@ -206,9 +153,6 @@ const NoteItem = ({ {item.conflicted ? ( <Icon name="alert-circle" - style={{ - marginRight: 6 - }} size={SIZE.sm} color={colors.error.accent} /> @@ -219,9 +163,6 @@ const NoteItem = ({ testID="sync-off" name="sync-off" size={SIZE.sm} - style={{ - marginRight: 6 - }} color={primaryColors.icon} /> ) : null} @@ -231,18 +172,14 @@ const NoteItem = ({ testID="pencil-lock" name="pencil-lock" size={SIZE.sm} - style={{ - marginRight: 6 - }} color={primaryColors.icon} /> ) : null} <TimeSince style={{ - fontSize: SIZE.xs, - color: colors.secondary.paragraph, - marginRight: 6 + fontSize: SIZE.xxs, + color: colors.secondary.paragraph }} time={date} updateFrequency={Date.now() - date < 60000 ? 2000 : 60000} @@ -253,7 +190,6 @@ const NoteItem = ({ style={{ flexDirection: "row", alignItems: "center", - marginRight: 6, gap: 2 }} > @@ -264,7 +200,7 @@ const NoteItem = ({ /> <Paragraph color={colors.secondary.paragraph} - size={SIZE.xs} + size={SIZE.xxs} > {attachmentsCount} </Paragraph> @@ -275,10 +211,7 @@ const NoteItem = ({ <Icon testID="icon-pinned" name="pin-outline" - size={SIZE.sm} - style={{ - marginRight: 6 - }} + size={SIZE.xs} color={color?.colorCode || primaryColors.accent} /> ) : null} @@ -288,9 +221,6 @@ const NoteItem = ({ name="lock" testID="note-locked-icon" size={SIZE.sm} - style={{ - marginRight: 6 - }} color={primaryColors.icon} /> ) : null} @@ -300,36 +230,80 @@ const NoteItem = ({ testID="icon-star" name="star-outline" size={SIZE.sm} - style={{ - marginRight: 6 - }} color="orange" /> ) : null} + {reminder ? ( + <ReminderTime + reminder={reminder} + disabled + color={color?.colorCode} + textStyle={{ + fontSize: SIZE.xxxs + }} + iconSize={SIZE.xxxs} + style={{ + height: "auto" + }} + /> + ) : null} + + {notebooks?.items + ?.filter( + (item) => + item.id !== useNavigationStore.getState().focusedRouteId + ) + .map((item) => ( + <View + key={item.id} + style={{ + borderRadius: 4, + backgroundColor: colors.secondary.background, + paddingHorizontal: 4, + borderWidth: 0.5, + borderColor: primaryColors.border, + paddingVertical: 1, + flexDirection: "row", + alignItems: "center", + gap: DefaultAppStyles.GAP_SMALL / 2 + }} + > + <AppIcon + name="book-outline" + size={SIZE.xxxs} + color={colors.secondary.icon} + /> + <Paragraph + size={SIZE.xxxs} + color={colors.secondary.paragraph} + > + {item.title} + </Paragraph> + </View> + ))} + {!isTrash && !compactMode && tags ? tags.items?.map((item) => item.id ? ( - <Button - title={"#" + item.title} + <View key={item.id} - height={23} - type="plain" - textStyle={{ - textDecorationLine: "underline", - color: colors.secondary.paragraph - }} - hitSlop={{ top: 8, bottom: 12, left: 0, right: 0 }} - fontSize={SIZE.xs} style={{ - borderRadius: 5, - paddingHorizontal: 6, - marginRight: 4, - zIndex: 10, - maxWidth: tags.items?.length > 1 ? 130 : null + borderRadius: 4, + backgroundColor: colors.secondary.background, + paddingHorizontal: 4, + borderWidth: 0.5, + borderColor: primaryColors.border, + paddingVertical: 1 }} - onPress={() => TaggedNotes.navigate(item, true)} - /> + > + <Paragraph + size={SIZE.xxxs} + color={colors.secondary.paragraph} + > + #{item.title} + </Paragraph> + </View> ) : null ) : null} @@ -338,7 +312,7 @@ const NoteItem = ({ <> <Paragraph color={colors.secondary.paragraph} - size={SIZE.xs} + size={SIZE.xxs} style={{ marginRight: 6 }} @@ -352,7 +326,7 @@ const NoteItem = ({ <Paragraph color={primaryColors.accent} - size={SIZE.xs} + size={SIZE.xxs} style={{ marginRight: 6 }} @@ -409,7 +383,7 @@ const NoteItem = ({ <TimeSince style={{ - fontSize: SIZE.xs, + fontSize: SIZE.xxs, color: colors.secondary.paragraph, marginRight: 6 }} @@ -419,20 +393,30 @@ const NoteItem = ({ </> ) : null} - <IconButton - testID={notesnook.listitem.menu} - color={primaryColors.paragraph} - name="dots-horizontal" - size={SIZE.xl} - onPress={() => !noOpen && Properties.present(item)} - style={{ - justifyContent: "center", - height: 35, - width: 35, - borderRadius: 100, - alignItems: "center" - }} - /> + {selectionMode ? ( + <> + <AppIcon + name={selected ? "checkbox-outline" : "checkbox-blank-outline"} + color={selected ? colors.selected.icon : colors.primary.icon} + size={SIZE.lg} + /> + </> + ) : ( + <IconButton + testID={notesnook.listitem.menu} + color={colors.secondary.icon} + name="dots-horizontal" + size={SIZE.lg} + onPress={() => !noOpen && Properties.present(item)} + style={{ + justifyContent: "center", + height: 35, + width: 35, + borderRadius: 100, + alignItems: "center" + }} + /> + )} </View> </> ); diff --git a/apps/mobile/app/components/list-items/note/wrapper.tsx b/apps/mobile/app/components/list-items/note/wrapper.tsx index cd9a1ecaf..153f68143 100644 --- a/apps/mobile/app/components/list-items/note/wrapper.tsx +++ b/apps/mobile/app/components/list-items/note/wrapper.tsx @@ -29,7 +29,7 @@ import { presentSheet } from "../../../services/event-manager"; import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events"; -import { tabBarRef } from "../../../utils/global-refs"; +import { fluidTabsRef } from "../../../utils/global-refs"; import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common"; import NotePreview from "../../note-history/preview"; @@ -66,7 +66,7 @@ export const openNote = async ( item: note }); if (!DDS.isTab) { - tabBarRef.current?.goToPage(1); + fluidTabsRef.current?.goToPage(1); } } }; @@ -101,6 +101,7 @@ export const NoteWrapper = React.memo< onPress={() => openNote(item as Note, isTrash, isRenderedInActionSheet)} isSheet={isRenderedInActionSheet} item={item} + index={index} color={restProps.color?.colorCode} > <NoteItem {...restProps} item={item} index={index} isTrash={isTrash} /> diff --git a/apps/mobile/app/components/list-items/selection-wrapper/back-fill.tsx b/apps/mobile/app/components/list-items/selection-wrapper/back-fill.tsx index c926d5151..8f1cb0724 100644 --- a/apps/mobile/app/components/list-items/selection-wrapper/back-fill.tsx +++ b/apps/mobile/app/components/list-items/selection-wrapper/back-fill.tsx @@ -39,16 +39,15 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => { style={{ position: "absolute", width: "110%", - height: "150%", - backgroundColor: colors.selected.background, - borderLeftWidth: 5, - borderLeftColor: isEditingNote - ? color - ? color - : colors.selected.accent - : "transparent" + height: "100%", + backgroundColor: colors.selected.accent + // borderLeftWidth: 5, + // borderLeftColor: isEditingNote + // ? color + // ? color + // : colors.selected.accent + // : "transparent" }} - collapsable={false} /> ) : null; }; diff --git a/apps/mobile/app/components/list-items/selection-wrapper/index.tsx b/apps/mobile/app/components/list-items/selection-wrapper/index.tsx index 49191153e..d37aa2b19 100644 --- a/apps/mobile/app/components/list-items/selection-wrapper/index.tsx +++ b/apps/mobile/app/components/list-items/selection-wrapper/index.tsx @@ -17,14 +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 { Item, TrashItem } from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; import React, { PropsWithChildren, useRef } from "react"; import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled"; +import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store"; import { useSelectionStore } from "../../../stores/use-selection-store"; +import { DefaultAppStyles } from "../../../utils/styles"; import { Pressable } from "../../ui/pressable"; -import { Filler } from "./back-fill"; -import { SelectionIcon } from "./selection"; -import { Item, TrashItem } from "@notesnook/core"; export function selectItem(item: Item) { if (useSelectionStore.getState().selectionMode === item.type) { @@ -49,6 +49,7 @@ type SelectionWrapperProps = PropsWithChildren<{ testID?: string; isSheet?: boolean; color?: string; + index?: number; }>; const SelectionWrapper = ({ @@ -57,10 +58,16 @@ const SelectionWrapper = ({ testID, isSheet, children, - color + color, + index = 0 }: SelectionWrapperProps) => { const itemId = useRef(item.id); const { colors, isDark } = useThemeColors(); + const isEditingNote = useTabStore( + (state) => + state.tabs.find((t) => t.id === state.currentTab)?.session?.noteId === + item.id + ); const compactMode = useIsCompactModeEnabled( (item as TrashItem).itemType || item.type ); @@ -79,7 +86,13 @@ const SelectionWrapper = ({ return ( <Pressable - customColor={isSheet ? colors.secondary.background : "transparent"} + customColor={ + isEditingNote + ? colors.selected.background + : isSheet + ? colors.primary.hover + : "transparent" + } testID={testID} onLongPress={onLongPress} onPress={onPress} @@ -91,15 +104,14 @@ const SelectionWrapper = ({ justifyContent: "space-between", alignItems: "center", width: "100%", + alignSelf: "center", overflow: "hidden", - paddingHorizontal: 12, - paddingVertical: compactMode ? 4 : 12, + paddingHorizontal: DefaultAppStyles.GAP, + paddingVertical: compactMode ? 4 : DefaultAppStyles.GAP_VERTICAL, borderRadius: isSheet ? 10 : 0, marginBottom: isSheet ? 12 : undefined }} > - {item.type === "note" ? <Filler item={item} color={color} /> : null} - <SelectionIcon item={item} /> {children} </Pressable> ); diff --git a/apps/mobile/app/components/list/card.tsx b/apps/mobile/app/components/list/card.tsx index 667b92ade..753246257 100644 --- a/apps/mobile/app/components/list/card.tsx +++ b/apps/mobile/app/components/list/card.tsx @@ -17,13 +17,13 @@ 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 { Dimensions, View } from "react-native"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { useMessageStore } from "../../stores/use-message-store"; -import { useThemeColors } from "@notesnook/theme"; -import { getContainerBorder, hexToRGBA } from "../../utils/colors"; import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; import { Pressable } from "../ui/pressable"; import Paragraph from "../ui/typography/paragraph"; @@ -38,19 +38,24 @@ export const Card = ({ color }: { color?: string }) => { (announcements && announcements.length) ? null : ( <View style={{ - width: "95%" + width: "100%", + paddingHorizontal: DefaultAppStyles.GAP, + marginTop: DefaultAppStyles.GAP }} > <Pressable onPress={messageBoardState.onPress} type="plain" style={{ - paddingVertical: 12, + paddingVertical: DefaultAppStyles.GAP_VERTICAL, flexDirection: "row", alignItems: "center", justifyContent: "space-between", - paddingHorizontal: 0, - width: "100%" + paddingHorizontal: DefaultAppStyles.GAP_SMALL, + width: "100%", + backgroundColor: colors.secondary.background, + borderWidth: 1, + borderColor: colors.primary.border }} > <View @@ -63,24 +68,14 @@ export const Card = ({ color }: { color?: string }) => { <View style={{ width: 40 * fontScale, - backgroundColor: - messageBoardState.type === "error" - ? hexToRGBA(colors.error.accent, 0.15) - : hexToRGBA(color, 0.15), height: 40 * fontScale, borderRadius: 100, alignItems: "center", - justifyContent: "center", - ...getContainerBorder( - messageBoardState.type === "error" - ? colors.error.accent - : color || colors.primary.accent, - 0.4 - ) + justifyContent: "center" }} > <Icon - size={SIZE.lg} + size={SIZE.xxxl} color={ messageBoardState.type === "error" ? colors.error.icon : color } @@ -95,9 +90,6 @@ export const Card = ({ color }: { color?: string }) => { marginRight: 10 }} > - <Paragraph color={colors.secondary.paragraph} size={SIZE.xs}> - {messageBoardState.message} - </Paragraph> <Paragraph style={{ flexWrap: "nowrap", @@ -107,6 +99,9 @@ export const Card = ({ color }: { color?: string }) => { > {messageBoardState.actionText} </Paragraph> + <Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}> + {messageBoardState.message} + </Paragraph> </View> </View> diff --git a/apps/mobile/app/components/list/index.tsx b/apps/mobile/app/components/list/index.tsx index b13d8eefc..0651bb27d 100644 --- a/apps/mobile/app/components/list/index.tsx +++ b/apps/mobile/app/components/list/index.tsx @@ -34,8 +34,7 @@ import Sync from "../../services/sync"; import { RouteName } from "../../stores/use-navigation-store"; import { useSettingStore } from "../../stores/use-setting-store"; import { eScrollEvent } from "../../utils/events"; -import { tabBarRef } from "../../utils/global-refs"; -import { Footer } from "../list-items/footer"; +import { fluidTabsRef } from "../../utils/global-refs"; import { Header } from "../list-items/headers/header"; import { Empty, PlaceholderData } from "./empty"; import { ListItemWrapper } from "./list-item.wrapper"; @@ -43,6 +42,7 @@ import { ListItemWrapper } from "./list-item.wrapper"; type ListProps = { data: VirtualizedGrouping<Item> | undefined; dataType: Item["type"]; + mode?: "drawer" | "sheet"; onRefresh?: () => void; loading?: boolean; headerTitle?: string; @@ -160,7 +160,7 @@ export default function List(props: ListProps) { onScroll={onListScroll} nestedScrollEnabled={true} onMomentumScrollEnd={() => { - tabBarRef.current?.unlock(); + fluidTabsRef.current?.unlock(); }} getItemType={(item: number, index: number) => { return props.data?.type(index); @@ -192,9 +192,6 @@ export default function List(props: ListProps) { /> ) : null } - ListFooterComponent={ - <Footer height={props.renderedInRoute === "Notebook" ? 300 : 150} /> - } ListHeaderComponent={ <> {props.CustomLisHeader ? ( diff --git a/apps/mobile/app/components/list/reorderable-list.tsx b/apps/mobile/app/components/list/reorderable-list.tsx index 21455676c..b6457825d 100644 --- a/apps/mobile/app/components/list/reorderable-list.tsx +++ b/apps/mobile/app/components/list/reorderable-list.tsx @@ -26,7 +26,7 @@ import { DraxListRenderItemContent } from "react-native-drax"; -import { tabBarRef } from "../../utils/global-refs"; +import { fluidTabsRef } from "../../utils/global-refs"; import { SIZE } from "../../utils/size"; import { useSideBarDraggingStore } from "../side-menu/dragging-store"; import { IconButton } from "../ui/icon-button"; @@ -61,9 +61,9 @@ function ReorderableList<T extends { id: string }>({ const listRef = useRef<FlatList | null>(null); if (dragging) { - tabBarRef.current?.lock(); + fluidTabsRef.current?.lock(); } else { - tabBarRef.current?.unlock(); + fluidTabsRef.current?.unlock(); } useEffect(() => { @@ -78,8 +78,7 @@ function ReorderableList<T extends { id: string }>({ return isHidden && !dragging ? null : ( <View style={{ - flexDirection: "row", - paddingHorizontal: 12 + flexDirection: "row" }} > <View diff --git a/apps/mobile/app/components/properties/index.js b/apps/mobile/app/components/properties/index.js index 96cb1c5e4..23245edad 100644 --- a/apps/mobile/app/components/properties/index.js +++ b/apps/mobile/app/components/properties/index.js @@ -16,18 +16,19 @@ 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 { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; import React from "react"; -import { Platform, View } from "react-native"; +import { View } from "react-native"; import { FlatList } from "react-native-actions-sheet"; import { db } from "../../common/database"; import { DDS } from "../../services/device-detection"; import { eSendEvent, presentSheet } from "../../services/event-manager"; import { ColorValues } from "../../utils/colors"; import { eOnLoadNote } from "../../utils/events"; +import { fluidTabsRef } from "../../utils/global-refs"; import { SIZE } from "../../utils/size"; import SheetProvider from "../sheet-provider"; -import { useSideBarDraggingStore } from "../side-menu/dragging-store"; import { IconButton } from "../ui/icon-button"; import { Pressable } from "../ui/pressable"; import { ReminderTime } from "../ui/reminder-time"; @@ -36,10 +37,8 @@ import Paragraph from "../ui/typography/paragraph"; import { DateMeta } from "./date-meta"; import { Items } from "./items"; import Notebooks from "./notebooks"; -import { Synced } from "./synced"; import { TagStrip, Tags } from "./tags"; -import { tabBarRef } from "../../utils/global-refs"; -import { strings } from "@notesnook/intl"; +import Icon from "react-native-vector-icons/MaterialCommunityIcons"; const Line = ({ top = 6, bottom = 6 }) => { const { colors } = useThemeColors(); @@ -101,7 +100,8 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => { style={{ flexDirection: "row", alignItems: "center", - flexShrink: 1 + flexShrink: 1, + gap: 5 }} > {item.type === "color" ? ( @@ -116,16 +116,15 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => { marginRight: 10 }} /> + ) : item.type === "tag" ? ( + <Icon + name="pound" + size={SIZE.lg} + color={colors.primary.icon} + /> ) : null} - <Heading size={SIZE.lg}> - {item.type === "tag" && !isColor ? ( - <Heading size={SIZE.xl} color={colors.primary.accent}> - # - </Heading> - ) : null} - {item.title} - </Heading> + <Heading size={SIZE.lg}>{item.title}</Heading> </View> {item.type === "note" ? ( @@ -144,7 +143,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => { newTab: true }); if (!DDS.isTab) { - tabBarRef.current?.goToPage(1); + fluidTabsRef.current?.goToPage(1); } }} /> @@ -202,8 +201,6 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => { }} /> - <Synced item={item} close={close} /> - {DDS.isTab ? ( <View style={{ @@ -218,87 +215,36 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => { ); }; -Properties.present = async (item, buttons = [], isSheet) => { +Properties.present = async (item, isSheet) => { if (!item) return; let type = item?.type; - let props = []; - let android = []; + let dbItem; switch (type) { case "trash": - props[0] = item; - props.push(["delete", "restore"]); + dbItem = item; break; case "note": - android = Platform.OS === "android" ? ["pin-to-notifications"] : []; - props[0] = await db.notes.note(item.id); - props.push([ - "notebooks", - "add-reminder", - "share", - "export", - "copy", - "publish", - "pin", - "favorite", - "attachments", - "lock-unlock", - "trash", - "remove-from-notebook", - "history", - "read-only", - "reminders", - "local-only", - "duplicate", - "copy-link", - "references", - ...android, - ...buttons - ]); + dbItem = await db.notes.note(item.id); break; case "notebook": - props[0] = await db.notebooks.notebook(item.id); - props.push([ - "edit-notebook", - "pin", - "add-shortcut", - "trash", - "default-notebook", - "add-notebook", - "move-notes", - "move-notebook" - ]); + dbItem = await db.notebooks.notebook(item.id); break; case "tag": - props[0] = await db.tags.tag(item.id); - props.push(["add-shortcut", "trash", "rename-tag"]); + dbItem = await db.tags.tag(item.id); break; case "color": - props[0] = await db.colors.color(item.id); - - props.push([ - "trash", - "rename-color", - ...(useSideBarDraggingStore.getState().dragging ? [] : ["reorder"]) - ]); + dbItem = await db.colors.color(item.id); break; case "reminder": { - props[0] = await db.reminders.reminder(item.id); - props.push(["edit-reminder", "trash", "disable-reminder"]); + dbItem = await db.reminders.reminder(item.id); break; } } - if (!props[0]) return; + presentSheet({ context: isSheet ? "local" : undefined, component: (ref, close) => ( - <Properties - close={() => { - close(); - }} - actionSheetRef={ref} - item={props[0]} - buttons={props[1]} - /> + <Properties close={close} actionSheetRef={ref} item={dbItem} /> ) }); }; diff --git a/apps/mobile/app/components/properties/items.js b/apps/mobile/app/components/properties/items.js deleted file mode 100644 index a13782acb..000000000 --- a/apps/mobile/app/components/properties/items.js +++ /dev/null @@ -1,266 +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 from "react"; -import { Dimensions, ScrollView, View } from "react-native"; -import Icon from "react-native-vector-icons/MaterialCommunityIcons"; -import { useActions } from "../../hooks/use-actions"; -import { useStoredRef } from "../../hooks/use-stored-ref"; -import { DDS } from "../../services/device-detection"; -import { useSettingStore } from "../../stores/use-setting-store"; -import { SIZE } from "../../utils/size"; -import { Button } from "../ui/button"; -import { Pressable } from "../ui/pressable"; -import Paragraph from "../ui/typography/paragraph"; - -export const Items = ({ item, buttons, close }) => { - const { colors } = useThemeColors(); - const topBarSorting = useStoredRef("topbar-sorting-ref", {}); - - const dimensions = useSettingStore((state) => state.dimensions); - const actions = useActions({ item, close }); - const data = actions.filter((i) => buttons.indexOf(i.id) > -1 && !i.hidden); - let width = dimensions.width > 600 ? 600 : dimensions.width; - const shouldShrink = - Dimensions.get("window").fontScale > 1 && - Dimensions.get("window").width < 450; - let columnItemsCount = DDS.isLargeTablet() ? 7 : shouldShrink ? 4 : 5; - let columnItemWidth = DDS.isTab - ? (width - 12) / columnItemsCount - : (width - 12) / columnItemsCount; - - const _renderRowItem = ({ item }) => ( - <View - key={item.id} - testID={"icon-" + item.id} - style={{ - alignItems: "center", - width: columnItemWidth, - marginBottom: 10 - }} - > - <Pressable - onPress={item.func} - type={item.on ? "shade" : "secondary"} - style={{ - height: columnItemWidth - 12, - width: columnItemWidth - 12, - borderRadius: 10, - justifyContent: "center", - alignItems: "center", - textAlign: "center", - textAlignVertical: "center", - marginBottom: DDS.isTab ? 7 : 3.5 - }} - > - <Icon - allowFontScaling - name={item.icon} - size={DDS.isTab ? SIZE.xxl : shouldShrink ? SIZE.xxl : SIZE.lg} - color={ - item.on - ? colors.primary.accent - : item.id.match(/(delete|trash)/g) - ? colors.error.icon - : colors.secondary.icon - } - /> - </Pressable> - - <Paragraph - size={SIZE.xs} - textBreakStrategy="simple" - style={{ textAlign: "center" }} - > - {item.title} - </Paragraph> - </View> - ); - - const renderColumnItem = (item) => ( - <Button - key={item.name + item.title} - buttonType={{ - text: item.on - ? colors.primary.accent - : item.name === "Delete" || item.name === "PermDelete" - ? colors.error.paragraph - : colors.primary.paragraph - }} - onPress={item.func} - title={item.title} - icon={item.icon} - type={item.on ? "shade" : "plain"} - fontSize={SIZE.sm} - style={{ - borderRadius: 0, - justifyContent: "flex-start", - alignSelf: "flex-start", - width: "100%" - }} - /> - ); - - const renderTopBarItem = (item, index) => { - return ( - <Pressable - onPress={() => { - item.func(); - setImmediate(() => { - const currentValue = topBarSorting.current[item.id] || 0; - topBarSorting.current = { - ...topBarSorting.current, - [item.id]: currentValue + 1 - }; - }); - }} - key={item.id} - testID={"icon-" + item.id} - activeOpacity={1} - style={{ - alignSelf: "flex-start", - paddingHorizontal: 0, - flex: 1 - }} - > - <View - onPress={item.func} - style={{ - height: topBarItemWidth, - justifyContent: "center", - alignItems: "center", - textAlign: "center", - textAlignVertical: "center", - marginBottom: DDS.isTab ? 7 : 3.5, - borderRadius: 100 - }} - > - <Icon - name={item.icon} - allowFontScaling - size={DDS.isTab ? SIZE.xxl : SIZE.md + 4} - color={ - item.on - ? colors.primary.accent - : item.name === "Delete" || item.name === "PermDelete" - ? colors.error.icon - : colors.secondary.icon - } - /> - </View> - - <Paragraph - textBreakStrategy="simple" - size={SIZE.xxs} - style={{ textAlign: "center" }} - > - {item.title} - </Paragraph> - </Pressable> - ); - }; - - const topBarItemsList = [ - "pin", - "favorite", - "copy", - "share", - "lock-unlock", - "publish", - "export", - "copy-link", - "duplicate", - "local-only", - "read-only" - ]; - - const bottomBarItemsList = [ - "notebooks", - "add-reminder", - "pin-to-notifications", - "history", - "reminders", - "attachments", - "references", - "trash" - ]; - - const topBarItems = data - .filter((item) => topBarItemsList.indexOf(item.id) > -1) - .sort((a, b) => - topBarItemsList.indexOf(a.id) > topBarItemsList.indexOf(b.id) ? 1 : -1 - ) - .sort((a, b) => { - return ( - (topBarSorting.current[b.id] || 0) - (topBarSorting.current[a.id] || 0) - ); - }); - - const bottomGridItems = data - .filter((item) => bottomBarItemsList.indexOf(item.id) > -1) - .sort((a, b) => - bottomBarItemsList.indexOf(a.id) > bottomBarItemsList.indexOf(b.id) - ? 1 - : -1 - ); - - let topBarItemWidth = - (width - (topBarItems.length * 10 + 14)) / topBarItems.length; - topBarItemWidth; - - if (topBarItemWidth < 60) { - topBarItemWidth = 60; - } - - return item.type === "note" ? ( - <> - <ScrollView - horizontal - style={{ - paddingHorizontal: 12, - marginTop: 6, - marginBottom: 6 - }} - showsHorizontalScrollIndicator={false} - contentContainerStyle={{ - paddingRight: 25, - gap: 15 - }} - > - {topBarItems.map(renderTopBarItem)} - </ScrollView> - - <View - style={{ - flexDirection: "row", - flexWrap: "wrap", - alignContent: "flex-start", - marginTop: item.type !== "note" ? 10 : 0, - paddingTop: 10, - marginLeft: 6 - }} - > - {bottomGridItems.map((item) => _renderRowItem({ item }))} - </View> - </> - ) : ( - <View data={data}>{data.map(renderColumnItem)}</View> - ); -}; diff --git a/apps/mobile/app/components/properties/items.tsx b/apps/mobile/app/components/properties/items.tsx new file mode 100644 index 000000000..9606ce0ad --- /dev/null +++ b/apps/mobile/app/components/properties/items.tsx @@ -0,0 +1,301 @@ +/* +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 { useThemeColors } from "@notesnook/theme"; +import React from "react"; +import { Dimensions, ScrollView, View } from "react-native"; +import Icon from "react-native-vector-icons/MaterialCommunityIcons"; +import { Action, ActionId, useActions } from "../../hooks/use-actions"; +import { useStoredRef } from "../../hooks/use-stored-ref"; +import { DDS } from "../../services/device-detection"; +import { useSettingStore } from "../../stores/use-setting-store"; +import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; +import { Button } from "../ui/button"; +import { Pressable } from "../ui/pressable"; +import Paragraph from "../ui/typography/paragraph"; + +const TOP_BAR_ITEMS: ActionId[] = [ + "pin", + "favorite", + "copy", + "share", + "lock-unlock", + "publish", + "export", + "copy-link", + "duplicate", + "local-only", + "read-only" +]; + +const BOTTOM_BAR_ITEMS: ActionId[] = [ + "notebooks", + "add-reminder", + "pin-to-notifications", + "history", + "reminders", + "attachments", + "references", + "trash" +]; + +const COLUMN_BAR_ITEMS: ActionId[] = [ + "select", + "add-notebook", + "edit-notebook", + "move-notes", + "move-notebook", + "pin", + "default-notebook", + "add-shortcut", + "trash" +]; + +export const Items = ({ item, close }: { item: Item; close: () => void }) => { + const { colors } = useThemeColors(); + const topBarSorting = useStoredRef<{ [name: string]: number }>( + "topbar-sorting-ref", + {} + ); + const dimensions = useSettingStore((state) => state.dimensions); + const actions = useActions({ item, close }); + const selectedActions = actions.filter((i) => !i.hidden); + const deviceMode = useSettingStore((state) => state.deviceMode); + const width = Math.min(dimensions.width, 600); + + const shouldShrink = + Dimensions.get("window").fontScale > 1 && + Dimensions.get("window").width < 450; + + const columnItemsCount = deviceMode === "tablet" ? 7 : shouldShrink ? 4 : 5; + + const columnItemWidth = + deviceMode !== "mobile" + ? (width - 16) / columnItemsCount + : (width - 16) / columnItemsCount; + + const topBarItems = selectedActions + .filter((item) => TOP_BAR_ITEMS.indexOf(item.id) > -1) + .sort((a, b) => + TOP_BAR_ITEMS.indexOf(a.id) > TOP_BAR_ITEMS.indexOf(b.id) ? 1 : -1 + ) + .sort((a, b) => { + return ( + (topBarSorting.current[b.id] || 0) - (topBarSorting.current[a.id] || 0) + ); + }); + + const bottomGridItems = selectedActions + .filter((item) => BOTTOM_BAR_ITEMS.indexOf(item.id) > -1) + .sort((a, b) => + BOTTOM_BAR_ITEMS.indexOf(a.id) > BOTTOM_BAR_ITEMS.indexOf(b.id) ? 1 : -1 + ); + + const columnItems = selectedActions + .filter((item) => COLUMN_BAR_ITEMS.indexOf(item.id) > -1) + .sort((a, b) => + COLUMN_BAR_ITEMS.indexOf(a.id) > COLUMN_BAR_ITEMS.indexOf(b.id) ? 1 : -1 + ); + + const topBarItemHeight = Math.min( + (width - (topBarItems.length * 10 + 14)) / topBarItems.length, + 60 + ); + + const renderRowItem = React.useCallback( + ({ item }: { item: Action }) => ( + <View + key={item.id} + testID={"icon-" + item.id} + style={{ + alignItems: "center", + width: columnItemWidth - 8 + }} + > + <Pressable + onPress={item.onPress} + type={item.checked ? "shade" : "secondary"} + style={{ + height: columnItemWidth - 8, + width: columnItemWidth - 8, + borderRadius: 10, + justifyContent: "center", + alignItems: "center", + marginBottom: DDS.isTab ? 7 : 3.5 + }} + > + <Icon + allowFontScaling + name={item.icon} + size={DDS.isTab ? SIZE.xxl : shouldShrink ? SIZE.xxl : SIZE.lg} + color={ + item.checked + ? item.activeColor || colors.primary.accent + : item.id.match(/(delete|trash)/g) + ? colors.error.icon + : colors.secondary.icon + } + /> + </Pressable> + + <Paragraph + size={SIZE.xxs} + textBreakStrategy="simple" + style={{ textAlign: "center" }} + > + {item.title} + </Paragraph> + </View> + ), + [ + colors.error.icon, + colors.primary.accent, + colors.secondary.icon, + columnItemWidth, + shouldShrink + ] + ); + + const renderColumnItem = React.useCallback( + (item: Action) => ( + <Button + key={item.id} + buttonType={{ + text: item.checked + ? item.activeColor || colors.primary.accent + : item.id === "delete" || item.id === "trash" + ? colors.error.paragraph + : colors.primary.paragraph + }} + onPress={item.onPress} + title={item.title} + icon={item.icon} + type={item.checked ? "inverted" : "plain"} + fontSize={SIZE.sm} + style={{ + borderRadius: 0, + justifyContent: "flex-start", + alignSelf: "flex-start", + width: "100%" + }} + /> + ), + [colors.error.paragraph, colors.primary.accent, colors.primary.paragraph] + ); + + const renderTopBarItem = React.useCallback( + (item: Action) => { + return ( + <Pressable + onPress={() => { + item.onPress(); + setImmediate(() => { + const currentValue = topBarSorting.current[item.id] || 0; + topBarSorting.current = { + ...topBarSorting.current, + [item.id]: currentValue + 1 + }; + }); + }} + key={item.id} + testID={"icon-" + item.id} + style={{ + alignSelf: "flex-start", + paddingHorizontal: 0, + flex: 1 + }} + > + <View + style={{ + height: topBarItemHeight, + justifyContent: "center", + alignItems: "center", + marginBottom: DDS.isTab ? 7 : 3.5, + borderRadius: 100 + }} + > + <Icon + name={item.icon} + allowFontScaling + size={DDS.isTab ? SIZE.xxl : SIZE.md + 4} + color={ + item.checked + ? item.activeColor || colors.primary.accent + : item.id === "delete" || item.id === "trash" + ? colors.error.icon + : colors.secondary.icon + } + /> + </View> + + <Paragraph + textBreakStrategy="simple" + size={SIZE.xxs} + style={{ textAlign: "center" }} + > + {item.title} + </Paragraph> + </Pressable> + ); + }, + [ + colors.error.icon, + colors.primary.accent, + colors.secondary.icon, + topBarItemHeight, + topBarSorting + ] + ); + + return item.type === "note" ? ( + <> + <ScrollView + horizontal + style={{ + paddingHorizontal: 16, + marginTop: 6, + marginBottom: 6 + }} + showsHorizontalScrollIndicator={false} + contentContainerStyle={{ + paddingRight: 25, + gap: 15 + }} + > + {topBarItems.map(renderTopBarItem)} + </ScrollView> + + <View + style={{ + flexDirection: "row", + flexWrap: "wrap", + marginTop: 10, + gap: 5, + paddingHorizontal: DefaultAppStyles.GAP + }} + > + {bottomGridItems.map((item) => renderRowItem({ item }))} + </View> + </> + ) : ( + <View>{columnItems.map(renderColumnItem)}</View> + ); +}; diff --git a/apps/mobile/app/components/properties/synced.js b/apps/mobile/app/components/properties/synced.js index f6cb05984..7778fee23 100644 --- a/apps/mobile/app/components/properties/synced.js +++ b/apps/mobile/app/components/properties/synced.js @@ -19,92 +19,44 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import { useThemeColors } from "@notesnook/theme"; import React from "react"; -import { View, useWindowDimensions } from "react-native"; -import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { useUserStore } from "../../stores/use-user-store"; import { openLinkInBrowser } from "../../utils/functions"; import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; import { sleep } from "../../utils/time"; import { Button } from "../ui/button"; -import Heading from "../ui/typography/heading"; -import Paragraph from "../ui/typography/paragraph"; -import { strings } from "@notesnook/intl"; export const Synced = ({ item, close }) => { const { colors } = useThemeColors(); const user = useUserStore((state) => state.user); const lastSynced = useUserStore((state) => state.lastSynced); - - const dimensions = useWindowDimensions(); - const shouldShrink = dimensions.fontScale > 1 && dimensions.width < 450; return user && lastSynced >= item.dateModified ? ( - <View + <Button style={{ - paddingVertical: 0, - width: "100%", - paddingHorizontal: 12, alignItems: "center", flexDirection: "row", justifyContent: "flex-start", - alignSelf: "center", - paddingTop: 10, - marginTop: 10, - borderTopWidth: 1, - borderTopColor: colors.primary.border + alignSelf: "flex-start", + height: "auto", + padding: DefaultAppStyles.GAP_VERTICAL_SMALL, + paddingHorizontal: DefaultAppStyles.GAP_SMALL }} - > - <Icon - name="shield-key-outline" - color={colors.primary.accent} - size={shouldShrink ? SIZE.xxl : SIZE.xxxl} - /> - - <View - style={{ - flex: 1, - marginLeft: 5, - flexShrink: 1 - }} - > - <Heading - color={colors.primary.heading} - size={SIZE.xs} - style={{ - flexWrap: "wrap" - }} - > - {strings.noteSyncedNoticeHeading()} - </Heading> - {shouldShrink ? null : ( - <Paragraph - style={{ - flexWrap: "wrap" - }} - size={SIZE.xs} - color={colors.primary.paragraph} - > - {strings.noteSyncedNoticeDesc(item.itemType || item.type)} - </Paragraph> - )} - </View> - - <Button - onPress={async () => { - try { - close(); - await sleep(300); - await openLinkInBrowser( - "https://help.notesnook.com/how-is-my-data-encrypted", - colors - ); - } catch (e) { - console.error(e); - } - }} - title={strings.learnMore()} - fontSize={SIZE.xs} - height={30} - type="secondaryAccented" - /> - </View> + fontSize={SIZE.xxxs} + iconSize={SIZE.xs} + icon="shield-key-outline" + type="shade" + title="Encrypted and synced" + onPress={async () => { + try { + close(); + await sleep(300); + await openLinkInBrowser( + "https://help.notesnook.com/how-is-my-data-encrypted", + colors + ); + } catch (e) { + console.error(e); + } + }} + /> ) : null; }; diff --git a/apps/mobile/app/components/selection-header/index.tsx b/apps/mobile/app/components/selection-header/index.tsx index 64a2667d6..af13a1e43 100644 --- a/apps/mobile/app/components/selection-header/index.tsx +++ b/apps/mobile/app/components/selection-header/index.tsx @@ -35,18 +35,17 @@ import Navigation from "../../services/navigation"; import useNavigationStore from "../../stores/use-navigation-store"; import { useSelectionStore } from "../../stores/use-selection-store"; import { deleteItems } from "../../utils/functions"; -import { tabBarRef } from "../../utils/global-refs"; +import { fluidTabsRef } from "../../utils/global-refs"; import { updateNotebook } from "../../utils/notebooks"; import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; import { sleep } from "../../utils/time"; import { presentDialog } from "../dialog/functions"; import MoveNoteSheet from "../sheets/add-to"; import ExportNotesSheet from "../sheets/export-notes"; import ManageTagsSheet from "../sheets/manage-tags"; import { MoveNotebookSheet } from "../sheets/move-notebook"; -import { Button } from "../ui/button"; import { IconButton } from "../ui/icon-button"; -import Heading from "../ui/typography/heading"; export const SelectionHeader = React.memo( ({ @@ -75,9 +74,9 @@ export const SelectionHeader = React.memo( useEffect(() => { if (selectionMode) { - tabBarRef.current?.lock(); + fluidTabsRef.current?.lock(); } else { - tabBarRef.current?.unlock(); + fluidTabsRef.current?.unlock(); } }, [selectionMode]); @@ -139,61 +138,24 @@ export const SelectionHeader = React.memo( <View style={{ width: "100%", - height: Platform.OS === "android" ? 50 + insets.top : 50, - paddingTop: Platform.OS === "android" ? insets.top : null, backgroundColor: colors.primary.background, - justifyContent: "space-between", + paddingVertical: DefaultAppStyles.GAP_VERTICAL, alignItems: "center", flexDirection: "row", zIndex: 999, - paddingHorizontal: 12 + paddingHorizontal: DefaultAppStyles.GAP, + position: "absolute", + bottom: 0, + borderTopWidth: 1, + borderColor: colors.primary.border, + justifyContent: "space-between" }} > <View style={{ flexDirection: "row", - justifyContent: "flex-start", alignItems: "center", - borderRadius: 100 - }} - > - <IconButton - style={{ - justifyContent: "center", - alignItems: "center", - height: 40, - width: 40, - borderRadius: 100, - marginRight: 10 - }} - onPress={() => { - clearSelection(); - }} - color={colors.primary.icon} - name="close" - /> - - <View - style={{ - height: 40, - borderRadius: 100, - paddingHorizontal: 16, - justifyContent: "center", - flexDirection: "row", - alignItems: "center" - }} - > - <Heading size={SIZE.lg} color={colors.primary.paragraph}> - {selectedItemsList.length} - </Heading> - </View> - </View> - - <View - style={{ - flexDirection: "row", - justifyContent: "flex-start", - alignItems: "center" + gap: DefaultAppStyles.GAP_SMALL }} > <IconButton @@ -202,42 +164,14 @@ export const SelectionHeader = React.memo( .getState() .setAll(allSelected ? [] : [...((await items?.ids()) || [])]); }} - tooltipText="Select all" - tooltipPosition={4} - style={{ - marginLeft: 10 - }} - color={ - allSelected ? colors.primary.accent : colors.primary.paragraph - } + size={SIZE.lg} + color={allSelected ? colors.primary.accent : colors.primary.icon} name="select-all" /> - {selectedItemsList.length ? ( - <Menu - ref={menuRef} - animationDuration={200} - style={{ - borderRadius: 5, - backgroundColor: contextMenuColors.primary.background, - marginTop: 35 - }} - onRequestClose={() => { - //@ts-ignore - menuRef.current?.hide(); - }} - anchor={ - <IconButton - onPress={() => { - //@ts-ignore - menuRef.current?.show(); - }} - name="dots-vertical" - color={colors.primary.paragraph} - /> - } - > - {[ + {!selectedItemsList.length + ? null + : [ { title: strings.move(), onPress: async () => { @@ -341,20 +275,12 @@ export const SelectionHeader = React.memo( } ].map((item) => !item.visible ? null : ( - <Button - style={{ - justifyContent: "flex-start", - borderRadius: 0, - alignSelf: "flex-start", - width: "100%" - }} + <IconButton + size={SIZE.lg} type="plain" - buttonType={{ - text: contextMenuColors.primary.paragraph - }} - icon={item.icon} + name={item.icon} key={item.title} - title={item.title} + color={colors.primary.icon} onPress={async () => { //@ts-ignore menuRef.current?.hide(); @@ -364,9 +290,16 @@ export const SelectionHeader = React.memo( /> ) )} - </Menu> - ) : null} </View> + + <IconButton + size={SIZE.lg} + onPress={() => { + clearSelection(); + }} + color={colors.primary.icon} + name="close" + /> </View> ); } diff --git a/apps/mobile/app/components/sheets/add-notebook/index.tsx b/apps/mobile/app/components/sheets/add-notebook/index.tsx index 62767287e..2821e7a88 100644 --- a/apps/mobile/app/components/sheets/add-notebook/index.tsx +++ b/apps/mobile/app/components/sheets/add-notebook/index.tsx @@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { Notebook } from "@notesnook/core"; +import { strings } from "@notesnook/intl"; import React, { useRef, useState } from "react"; import { TextInput, View } from "react-native"; import { notesnook } from "../../../../e2e/test.ids"; @@ -30,17 +31,16 @@ import { } from "../../../services/event-manager"; import Navigation from "../../../services/navigation"; import { useMenuStore } from "../../../stores/use-menu-store"; +import { useNotebookStore } from "../../../stores/use-notebook-store"; import { useRelationStore } from "../../../stores/use-relation-store"; +import { eOnNotebookUpdated } from "../../../utils/events"; +import { getParentNotebookId } from "../../../utils/notebooks"; import { SIZE } from "../../../utils/size"; import { Button } from "../../ui/button"; import Input from "../../ui/input"; 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"; -import { useNotebookStore } from "../../../stores/use-notebook-store"; -import { strings } from "@notesnook/intl"; export const AddNotebookSheet = ({ notebook, @@ -97,11 +97,10 @@ export const AddNotebookSheet = ({ parentNotebook?.id || (await getParentNotebookId(notebook?.id || (id as string))); - eSendEvent(eOnNotebookUpdated, parent); - if (notebook) { - setImmediate(() => { - eSendEvent(eOnNotebookUpdated, notebook.id); - }); + eSendEvent(eOnNotebookUpdated, parent || notebook?.id); + + if (!parent) { + useNotebookStore.getState().refresh(); } if (!notebook && showMoveNotesOnComplete && id) { diff --git a/apps/mobile/app/components/sheets/move-notebook/index.tsx b/apps/mobile/app/components/sheets/move-notebook/index.tsx index 57e05cb47..14640ee3b 100644 --- a/apps/mobile/app/components/sheets/move-notebook/index.tsx +++ b/apps/mobile/app/components/sheets/move-notebook/index.tsx @@ -17,6 +17,7 @@ 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 { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; import React, { useCallback, useEffect, useState } from "react"; import { Text, View } from "react-native"; @@ -45,7 +46,6 @@ import { Pressable } from "../../ui/pressable"; import Seperator from "../../ui/seperator"; import Paragraph from "../../ui/typography/paragraph"; import { AddNotebookSheet } from "../add-notebook"; -import { strings } from "@notesnook/intl"; const useNotebookExpandedStore = create<{ expanded: { @@ -119,14 +119,19 @@ export const MoveNotebookSheet = ({ }, notebook ); + } + await db.relations.add(selectedNotebook, notebook); + if (parent) { eSendEvent(eOnNotebookUpdated, parent); } - - await db.relations.add(selectedNotebook, notebook); - eSendEvent(eOnNotebookUpdated, selectedNotebook.id); - eSendEvent(eOnNotebookUpdated, notebook.id); } - useNotebookStore.getState().refresh(); + + if (!parent) { + useNotebookStore.getState().refresh(); + } else { + eSendEvent(eOnNotebookUpdated, selectedNotebook.id); + } + close?.(); } }); diff --git a/apps/mobile/app/components/sheets/references/index.tsx b/apps/mobile/app/components/sheets/references/index.tsx index 484a304ce..13f5caef9 100644 --- a/apps/mobile/app/components/sheets/references/index.tsx +++ b/apps/mobile/app/components/sheets/references/index.tsx @@ -34,7 +34,7 @@ import { useDBItem, useNoteLocked } from "../../../hooks/use-db-item"; import { eSendEvent, presentSheet } from "../../../services/event-manager"; import { useRelationStore } from "../../../stores/use-relation-store"; import { eOnLoadNote } from "../../../utils/events"; -import { tabBarRef } from "../../../utils/global-refs"; +import { fluidTabsRef } from "../../../utils/global-refs"; import { SIZE } from "../../../utils/size"; import SheetProvider from "../../sheet-provider"; import { Button } from "../../ui/button"; @@ -423,7 +423,7 @@ export const ReferencesList = ({ item, close }: ReferencesListProps) => { item: note, blockId: blockId }); - tabBarRef.current?.goToPage(1); + fluidTabsRef.current?.goToPage(1); close?.(); }} reference={item as Note} diff --git a/apps/mobile/app/components/sheets/sort/index.js b/apps/mobile/app/components/sheets/sort/index.tsx similarity index 50% rename from apps/mobile/app/components/sheets/sort/index.js rename to apps/mobile/app/components/sheets/sort/index.tsx index 49787094a..fa303bb1a 100644 --- a/apps/mobile/app/components/sheets/sort/index.js +++ b/apps/mobile/app/components/sheets/sort/index.tsx @@ -17,45 +17,55 @@ 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 { + GroupingKey, + GroupOptions, + ItemType, + SortOptions +} from "@notesnook/core"; +import { strings } from "@notesnook/intl"; +import { useThemeColors } from "@notesnook/theme"; import React, { useState } from "react"; import { View } from "react-native"; import { db } from "../../../common/database"; import { eSendEvent } from "../../../services/event-manager"; import Navigation from "../../../services/navigation"; -import { useThemeColors } from "@notesnook/theme"; +import { RouteName } from "../../../stores/use-navigation-store"; import { GROUP, SORT } from "../../../utils/constants"; import { eGroupOptionsUpdated, refreshNotesPage } from "../../../utils/events"; import { SIZE } from "../../../utils/size"; +import { DefaultAppStyles } from "../../../utils/styles"; +import AppIcon from "../../ui/AppIcon"; import { Button } from "../../ui/button"; -import Seperator from "../../ui/seperator"; +import { Pressable } from "../../ui/pressable"; import Heading from "../../ui/typography/heading"; -import { strings } from "@notesnook/intl"; -const Sort = ({ type, screen }) => { - const isTopicSheet = screen === "TopicSheet"; +import Paragraph from "../../ui/typography/paragraph"; +const Sort = ({ type, screen }: { type: ItemType; screen: RouteName }) => { const { colors } = useThemeColors(); const [groupOptions, setGroupOptions] = useState( - db.settings.getGroupOptions(screen === "Notes" ? "home" : type + "s") + db.settings.getGroupOptions( + screen === "Notes" ? "home" : ((type + "s") as GroupingKey) + ) ); - const updateGroupOptions = async (_groupOptions) => { - const groupType = screen === "Notes" ? "home" : type + "s"; - + const updateGroupOptions = async (_groupOptions: GroupOptions) => { + const groupType = + screen === "Notes" ? "home" : ((type + "s") as GroupingKey); + console.log("updateGroupOptions for group", groupType, "in", screen); await db.settings.setGroupOptions(groupType, _groupOptions); setGroupOptions(_groupOptions); setTimeout(() => { - if (screen !== "TopicSheet") Navigation.queueRoutesForUpdate(screen); + Navigation.queueRoutesForUpdate(screen); eSendEvent(eGroupOptionsUpdated, groupType); eSendEvent(refreshNotesPage); }, 1); }; const setOrderBy = async () => { - let _groupOptions = { + const _groupOptions: GroupOptions = { ...groupOptions, sortDirection: groupOptions?.sortDirection === "asc" ? "desc" : "asc" }; - if (type === "topics") { - _groupOptions.groupBy = "none"; - } + await updateGroupOptions(_groupOptions); }; @@ -64,7 +74,8 @@ const Sort = ({ type, screen }) => { style={{ width: "100%", backgroundColor: colors.primary.background, - justifyContent: "space-between" + justifyContent: "space-between", + gap: DefaultAppStyles.GAP_SMALL }} > <View @@ -72,11 +83,12 @@ const Sort = ({ type, screen }) => { flexDirection: "row", alignItems: "center", justifyContent: "space-between", - paddingHorizontal: 12 + paddingHorizontal: DefaultAppStyles.GAP, + paddingVertical: DefaultAppStyles.GAP_VERTICAL }} > <Heading - size={SIZE.xl} + size={SIZE.md} style={{ alignSelf: "center" }} @@ -105,34 +117,22 @@ const Sort = ({ type, screen }) => { ? "sort-ascending" : "sort-descending" } - height={25} + height={30} iconPosition="right" - fontSize={SIZE.sm - 1} - type="transparent" - buttonType={{ - text: colors.primary.accent - }} + fontSize={SIZE.sm} + type="plain" style={{ - borderRadius: 100, - paddingHorizontal: 6 + paddingHorizontal: DefaultAppStyles.GAP_SMALL }} onPress={setOrderBy} /> </View> - <Seperator /> - <View style={{ - flexDirection: "row", + flexDirection: "column", justifyContent: "flex-start", - flexWrap: "wrap", - borderBottomWidth: isTopicSheet ? 0 : 1, - borderBottomColor: colors.primary.border, - marginBottom: 12, - paddingHorizontal: 12, - paddingBottom: 12, - alignItems: "center" + borderBottomColor: colors.primary.border }} > {groupOptions?.groupBy === "abc" ? ( @@ -149,105 +149,118 @@ const Sort = ({ type, screen }) => { ) : ( Object.keys(SORT).map((item) => (item === "dueDate" && screen !== "Reminders") || - ((screen !== "Tags" || screen !== "Reminders") && + (screen !== "Tags" && + screen !== "Reminders" && item === "dateModified") || ((screen === "Tags" || screen === "Reminders") && item === "dateEdited") ? null : ( - <Button + <Pressable key={item} type={groupOptions?.sortBy === item ? "selected" : "plain"} - title={strings.sortByStrings[item]()} - height={40} - iconPosition="left" - icon={groupOptions?.sortBy === item ? "check" : null} + noborder style={{ - marginRight: 10, - paddingHorizontal: 8 + width: "100%", + justifyContent: "space-between", + height: 40, + flexDirection: "row", + borderRadius: 0, + paddingHorizontal: DefaultAppStyles.GAP }} - buttonType={{ - text: - groupOptions?.sortBy === item - ? colors.primary.accent - : colors.secondary.paragraph - }} - fontSize={SIZE.sm} onPress={async () => { - let _groupOptions = { + const _groupOptions: GroupOptions = { ...groupOptions, - sortBy: type === "trash" ? "dateDeleted" : item + sortBy: + type === "trash" + ? "dateDeleted" + : (item as SortOptions["sortBy"]) }; - if (screen === "TopicSheet") { - _groupOptions.groupBy = "none"; - } await updateGroupOptions(_groupOptions); }} - iconSize={SIZE.md} - /> + > + <Paragraph> + {strings.sortByStrings[ + item as keyof typeof strings.sortByStrings + ]()} + </Paragraph> + + {groupOptions.sortBy === item ? ( + <AppIcon + size={SIZE.lg} + name="check" + color={colors.selected.accent} + /> + ) : null} + </Pressable> ) ) )} </View> - {isTopicSheet ? null : ( - <> - <Heading - style={{ - marginLeft: 12 - }} - size={SIZE.lg} - > - {strings.groupBy()} - </Heading> + <> + <View + style={{ + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: DefaultAppStyles.GAP, + paddingVertical: DefaultAppStyles.GAP_VERTICAL + }} + > + <Heading size={SIZE.md}>{strings.groupBy()}</Heading> + </View> - <Seperator /> + <View + style={{ + borderRadius: 0, + flexDirection: "row", + flexWrap: "wrap" + }} + > + {Object.keys(GROUP).map((item) => ( + <Pressable + key={item} + type={ + groupOptions?.groupBy === GROUP[item as keyof typeof GROUP] + ? "selected" + : "plain" + } + noborder + style={{ + width: "100%", + justifyContent: "space-between", + height: 40, + flexDirection: "row", + borderRadius: 0, + paddingHorizontal: DefaultAppStyles.GAP + }} + onPress={async () => { + const _groupOptions: GroupOptions = { + ...groupOptions, + sortBy: + type === "trash" + ? "dateDeleted" + : (item as SortOptions["sortBy"]) + }; + await updateGroupOptions(_groupOptions); + }} + > + <Paragraph> + {strings.groupByStrings[ + item as keyof typeof strings.groupByStrings + ]()} + </Paragraph> - <View - style={{ - borderRadius: 0, - flexDirection: "row", - flexWrap: "wrap", - paddingHorizontal: 12 - }} - > - {Object.keys(GROUP).map((item) => ( - <Button - key={item} - testID={"btn-" + item} - type={ - groupOptions?.groupBy === GROUP[item] ? "selected" : "plain" - } - buttonType={{ - text: - groupOptions?.groupBy === GROUP[item] - ? colors.primary.accent - : colors.secondary.paragraph - }} - onPress={async () => { - let _groupOptions = { - ...groupOptions, - groupBy: GROUP[item] - }; - - if (item === "abc") { - _groupOptions.sortBy = "title"; - _groupOptions.sortDirection = "desc"; - } - - updateGroupOptions(_groupOptions); - }} - height={40} - icon={groupOptions?.groupBy === GROUP[item] ? "check" : null} - title={strings.groupByStrings[item]()} - style={{ - paddingHorizontal: 8, - marginBottom: 10, - marginRight: 10 - }} - /> - ))} - </View> - </> - )} + {groupOptions.groupBy === item ? ( + <AppIcon + size={SIZE.lg} + name="check" + color={colors.selected.accent} + /> + ) : null} + </Pressable> + ))} + </View> + </> </View> ); }; diff --git a/apps/mobile/app/components/sheets/user/index.tsx b/apps/mobile/app/components/sheets/user/index.tsx new file mode 100644 index 000000000..686139dc3 --- /dev/null +++ b/apps/mobile/app/components/sheets/user/index.tsx @@ -0,0 +1,304 @@ +/* +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 { strings } from "@notesnook/intl"; +import { useThemeColors } from "@notesnook/theme"; +import { useNetInfo } from "@react-native-community/netinfo"; +import React from "react"; +import { ActivityIndicator, Image, View } from "react-native"; +import { useSheetRef } from "react-native-actions-sheet"; +import useSyncProgress from "../../../hooks/use-sync-progress"; +import { presentSheet } from "../../../services/event-manager"; +import Navigation from "../../../services/navigation"; +import { SyncStatus, useUserStore } from "../../../stores/use-user-store"; +import { SIZE } from "../../../utils/size"; +import { DefaultAppStyles } from "../../../utils/styles"; +import { Card } from "../../list/card"; +import AppIcon from "../../ui/AppIcon"; +import { Button } from "../../ui/button"; +import { Pressable } from "../../ui/pressable"; +import { TimeSince } from "../../ui/time-since"; +import Paragraph from "../../ui/typography/paragraph"; + +export const UserSheet = () => { + const ref = useSheetRef(); + const { colors } = useThemeColors(); + const [userProfile, user, syncing, lastSyncStatus, lastSynced] = useUserStore( + (state) => [ + state.profile, + state.user, + state.syncing, + state.lastSyncStatus, + state.lastSynced + ] + ); + + const { isInternetReachable } = useNetInfo(); + const isOffline = !isInternetReachable; + const { progress } = useSyncProgress(); + + return ( + <View + style={{ + width: "100%", + justifyContent: "center", + paddingHorizontal: DefaultAppStyles.GAP, + gap: DefaultAppStyles.GAP + }} + > + {user ? ( + <View + style={{ + flexDirection: "row", + alignItems: "center" + }} + > + <Image + source={{ + uri: userProfile?.profilePicture + }} + style={{ + width: 40, + height: 40, + borderRadius: 10 + }} + /> + + <View + style={{ + flex: 1, + flexDirection: "row", + justifyContent: "space-between" + }} + > + <View style={{ marginLeft: 10 }}> + <Paragraph size={SIZE.xs}>{userProfile?.fullName}</Paragraph> + <Paragraph + style={{ + flexWrap: "wrap" + }} + size={SIZE.xxs} + color={colors.secondary.heading} + > + <AppIcon + name="checkbox-blank-circle" + size={10} + allowFontScaling + color={ + !user || lastSyncStatus === SyncStatus.Failed + ? colors.error.icon + : isOffline + ? colors.static.orange + : colors.success.icon + } + />{" "} + {!user ? ( + strings.notLoggedIn() + ) : lastSynced && lastSynced !== "Never" ? ( + <> + {syncing + ? `${strings.syncing()} ${ + progress ? `(${progress.current})` : "" + }` + : lastSyncStatus === SyncStatus.Failed + ? strings.syncFailed() + : strings.synced()}{" "} + {!syncing ? ( + <TimeSince + style={{ + fontSize: SIZE.xxs, + color: colors.secondary.paragraph + }} + updateFrequency={30 * 1000} + time={lastSynced as number} + /> + ) : null} + {isOffline ? ` (${strings.offline()})` : ""} + </> + ) : ( + strings.never() + )} + </Paragraph> + </View> + {syncing ? ( + <ActivityIndicator + color={colors.primary.accent} + size={SIZE.xxl} + /> + ) : null} + </View> + </View> + ) : ( + <View + style={{ + width: "100%" + }} + > + <Card /> + </View> + )} + + {user ? ( + <View + style={{ + paddingVertical: DefaultAppStyles.GAP_SMALL, + gap: DefaultAppStyles.GAP, + borderRadius: 10, + backgroundColor: colors.primary.background + }} + > + <View + style={{ + gap: DefaultAppStyles.GAP_SMALL, + paddingHorizontal: DefaultAppStyles.GAP_SMALL + }} + > + <View + style={{ + flexDirection: "row", + width: "100%", + justifyContent: "space-between" + }} + > + <Paragraph size={SIZE.xxs}>{strings.storage()}</Paragraph> + <Paragraph size={SIZE.xxs}>50/100MB {strings.used()}</Paragraph> + </View> + <View + style={{ + backgroundColor: colors.secondary.background, + width: "100%", + height: 5, + borderRadius: 10 + }} + > + <View + style={{ + backgroundColor: colors.static.black, + height: 5, + width: "50%", + borderRadius: 10 + }} + /> + </View> + </View> + + <View + style={{ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + width: "100%", + padding: DefaultAppStyles.GAP_SMALL, + borderRadius: 10 + }} + > + <View> + <Paragraph size={SIZE.sm}>{strings.freePlan()}</Paragraph> + <Paragraph color={colors.secondary.paragraph} size={SIZE.xxxs}> + {strings.viewAllLimits()} + <AppIcon name="information" size={SIZE.xxxs} /> + </Paragraph> + </View> + + <Button + title={strings.upgradeNow()} + onPress={() => {}} + type="accent" + fontSize={SIZE.xs} + style={{ + paddingHorizontal: DefaultAppStyles.GAP_SMALL, + height: "auto", + paddingVertical: DefaultAppStyles.GAP_SMALL + }} + /> + </View> + </View> + ) : null} + <View + style={{ + borderBottomWidth: 1, + height: 1, + borderColor: colors.primary.border + }} + ></View> + + <View + style={{ + gap: DefaultAppStyles.GAP_VERTICAL + }} + > + {[ + { + icon: "account-outline", + title: strings.editProfile(), + onPress: () => {}, + hidden: !user + }, + { + icon: "cog-outline", + title: strings.settings(), + onPress: () => { + ref.current?.hide(); + Navigation.closeDrawer(); + Navigation.navigate("Settings"); + } + }, + { + icon: "logout", + title: strings.logout(), + onPress: () => { + ref.current?.hide(); + }, + hidden: !user + } + ].map((item) => + item.hidden ? null : ( + <Pressable + key={item.title} + style={{ + paddingVertical: DefaultAppStyles.GAP_SMALL, + alignItems: "center", + flexDirection: "row", + justifyContent: "flex-start", + gap: DefaultAppStyles.GAP_SMALL, + paddingHorizontal: DefaultAppStyles.GAP_SMALL + }} + onPress={() => { + item.onPress(); + }} + > + <AppIcon + color={colors.secondary.icon} + name={item.icon} + size={SIZE.xl} + /> + <Paragraph>{item.title}</Paragraph> + </Pressable> + ) + )} + </View> + </View> + ); +}; + +UserSheet.present = () => { + presentSheet({ + component: <UserSheet /> + }); +}; diff --git a/apps/mobile/app/components/side-menu/color-section.tsx b/apps/mobile/app/components/side-menu/color-section.tsx index 3a6700292..5040f7617 100644 --- a/apps/mobile/app/components/side-menu/color-section.tsx +++ b/apps/mobile/app/components/side-menu/color-section.tsx @@ -18,22 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { Color } from "@notesnook/core"; -import { useThemeColors } from "@notesnook/theme"; -import React, { useEffect } from "react"; +import React, { useEffect, useMemo } from "react"; import { View } from "react-native"; import { db } from "../../common/database"; import { ColoredNotes } from "../../screens/notes/colored"; import Navigation from "../../services/navigation"; import { useMenuStore } from "../../stores/use-menu-store"; -import useNavigationStore from "../../stores/use-navigation-store"; import { useSettingStore } from "../../stores/use-setting-store"; -import { SIZE, normalize } from "../../utils/size"; +import { SideMenuItem } from "../../utils/menu-items"; import ReorderableList from "../list/reorderable-list"; import { Properties } from "../properties"; -import { Pressable } from "../ui/pressable"; -import Heading from "../ui/typography/heading"; -import Paragraph from "../ui/typography/paragraph"; import { useSideBarDraggingStore } from "./dragging-store"; +import { MenuItem } from "./menu-item"; export const ColorSection = React.memo( function ColorSection() { @@ -54,6 +50,54 @@ export const ColorSection = React.memo( } }, [loading, setColorNotes]); + const onPress = React.useCallback((item: SideMenuItem) => { + ColoredNotes.navigate(item.data as Color, false); + setImmediate(() => { + Navigation.closeDrawer(); + }); + }, []); + + const onLongPress = React.useCallback((item: SideMenuItem) => { + if (useSideBarDraggingStore.getState().dragging) return; + Properties.present(item.data as Color); + }, []); + + const renderIcon = React.useCallback((item: SideMenuItem, size: number) => { + return ( + <View + style={{ + width: size, + height: size, + justifyContent: "center", + alignItems: "center" + }} + > + <View + style={{ + width: size - 5, + height: size - 5, + backgroundColor: (item.data as Color).colorCode, + borderRadius: 100 + }} + /> + </View> + ); + }, []); + + const menuItems = useMemo( + () => + colorNotes.map((item) => ({ + id: item.id, + title: item.title, + icon: "circle", + dataType: "color", + data: item, + onPress: onPress, + onLongPress: onLongPress + })) as SideMenuItem[], + [colorNotes, onPress, onLongPress] + ); + return ( <ReorderableList onListOrderChanged={(data) => { @@ -66,103 +110,16 @@ export const ColorSection = React.memo( itemOrder={order} hiddenItems={hiddensItems} alwaysBounceVertical={false} - data={colorNotes} + data={menuItems} style={{ width: "100%" }} showsVerticalScrollIndicator={false} renderDraggableItem={({ item }) => { - return <ColorItem key={item.id} item={item} />; + return <MenuItem item={item} renderIcon={renderIcon} />; }} /> ); }, () => true ); - -const ColorItem = React.memo( - function ColorItem({ item }: { item: Color }) { - const { colors, isDark } = useThemeColors(); - const isFocused = useNavigationStore( - (state) => state.focusedRouteId === item.id - ); - - const onPress = (item: Color) => { - ColoredNotes.navigate(item, false); - - setImmediate(() => { - Navigation.closeDrawer(); - }); - }; - - const onLongPress = () => { - if (useSideBarDraggingStore.getState().dragging) return; - Properties.present(item); - }; - - return ( - <Pressable - customColor={isFocused ? "rgba(0,0,0,0.04)" : "transparent"} - onLongPress={onLongPress} - customSelectedColor={item.colorCode} - customAlpha={!isDark ? -0.02 : 0.02} - customOpacity={0.12} - onPress={() => onPress(item)} - style={{ - width: "100%", - alignSelf: "center", - borderRadius: 5, - flexDirection: "row", - paddingHorizontal: 8, - justifyContent: "space-between", - alignItems: "center", - height: normalize(50), - marginBottom: 5 - }} - > - <View - style={{ - flexDirection: "row", - alignItems: "center" - }} - > - <View - style={{ - width: 30, - justifyContent: "center", - alignItems: "flex-start" - }} - > - <View - style={{ - width: SIZE.lg - 2, - height: SIZE.lg - 2, - backgroundColor: item.colorCode, - borderRadius: 100, - justifyContent: "center", - marginRight: 10 - }} - /> - </View> - {isFocused ? ( - <Heading color={colors.selected.heading} size={SIZE.md}> - {item.title} - </Heading> - ) : ( - <Paragraph color={colors.primary.paragraph} size={SIZE.md}> - {item.title} - </Paragraph> - )} - </View> - </Pressable> - ); - }, - (prev, next) => { - if (!next.item) return false; - if (prev.item?.title !== next.item.title) return false; - if (prev.item?.dateModified !== next.item?.dateModified) return false; - if (prev.item?.id !== next.item?.id) return false; - - return true; - } -); diff --git a/apps/mobile/app/components/side-menu/index.tsx b/apps/mobile/app/components/side-menu/index.tsx index dc3529b39..37a259d83 100644 --- a/apps/mobile/app/components/side-menu/index.tsx +++ b/apps/mobile/app/components/side-menu/index.tsx @@ -18,162 +18,351 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { useThemeColors } from "@notesnook/theme"; -import React, { useCallback } from "react"; -import { View } from "react-native"; -import { DraxProvider, DraxScrollView } from "react-native-drax"; +import React from "react"; +import { Image, View } from "react-native"; +import { + NavigationState, + Route, + SceneMap, + SceneRendererProps, + TabDescriptor, + TabView +} from "react-native-tab-view"; +import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { db } from "../../common/database"; -import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; -import { eSendEvent } from "../../services/event-manager"; -import { useMenuStore } from "../../stores/use-menu-store"; -import { useSettingStore } from "../../stores/use-setting-store"; +import Navigation from "../../services/navigation"; import { useUserStore } from "../../stores/use-user-store"; -import { SUBSCRIPTION_STATUS } from "../../utils/constants"; -import { eOpenPremiumDialog } from "../../utils/events"; -import { MenuItemsList } from "../../utils/menu-items"; -import ReorderableList from "../list/reorderable-list"; -import { Button } from "../ui/button"; -import { ColorSection } from "./color-section"; -import { useSideBarDraggingStore } from "./dragging-store"; -import { MenuItem } from "./menu-item"; -import { PinnedSection } from "./pinned-section"; -import { UserStatus } from "./user-status"; -import { strings } from "@notesnook/intl"; +import { deleteItems } from "../../utils/functions"; +import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; +import { MoveNotebookSheet } from "../sheets/move-notebook"; +import { UserSheet } from "../sheets/user"; +import { Pressable } from "../ui/pressable"; +import Paragraph from "../ui/typography/paragraph"; +import { SideMenuHome } from "./side-menu-home"; +import { SideMenuNotebooks } from "./side-menu-notebooks"; +import { SideMenuTags } from "./side-menu-tags"; +import { + useSideMenuNotebookSelectionStore, + useSideMenuTagsSelectionStore +} from "./stores"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { rootNavigatorRef } from "../../utils/global-refs"; +const renderScene = SceneMap({ + home: SideMenuHome, + notebooks: SideMenuNotebooks, + tags: SideMenuTags, + settings: () => null +}); export const SideMenu = React.memo( function SideMenu() { - const { colors, isDark } = useThemeColors(); - const insets = useGlobalSafeAreaInsets(); - const subscriptionType = useUserStore( - (state) => state.user?.subscription?.type - ); - const isAppLoading = useSettingStore((state) => state.isAppLoading); - const dragging = useSideBarDraggingStore((state) => state.dragging); - const [order, hiddensItems] = useMenuStore((state) => [ - state.order["routes"], - state.hiddenItems["routes"] + const { colors } = useThemeColors(); + const [index, setIndex] = React.useState(0); + const [routes] = React.useState<Route[]>([ + { + key: "home", + title: "Home" + }, + { + key: "notebooks", + title: "Notebooks" + }, + { + key: "tags", + title: "Tags" + }, + { + key: "settings", + title: "Settings" + } ]); - const introCompleted = useSettingStore( - (state) => state.settings.introCompleted - ); - - const pro = { - name: "Notesnook Pro", - icon: "crown", - func: () => { - eSendEvent(eOpenPremiumDialog); - } - }; - - const renderItem = useCallback( - () => ( - <> - <ReorderableList - onListOrderChanged={(data) => { - db.settings.setSideBarOrder("routes", data); - }} - onHiddenItemsChanged={(data) => { - db.settings.setSideBarHiddenItems("routes", data); - }} - itemOrder={order} - hiddenItems={hiddensItems} - alwaysBounceVertical={false} - data={MenuItemsList} - style={{ - width: "100%" - }} - showsVerticalScrollIndicator={false} - renderDraggableItem={({ item, index }) => { - return ( - <MenuItem - key={item.name} - item={{ - ...item, - title: - strings.routes[ - item.name as keyof typeof strings.routes - ]?.() || item.name - }} - testID={item.name} - index={index} - /> - ); - }} - /> - <ColorSection /> - <PinnedSection /> - </> - ), - [order, hiddensItems] - ); - - return !isAppLoading && introCompleted ? ( - <View + return ( + <SafeAreaView style={{ - height: "100%", - width: "100%", + flex: 1, backgroundColor: colors.primary.background }} > - <View - style={{ - height: "100%", - width: "100%", - backgroundColor: colors.primary.background, - paddingTop: insets.top - }} - > - <DraxProvider> - <DraxScrollView nestedScrollEnabled={false}> - {renderItem()} - </DraxScrollView> - </DraxProvider> - - <View - style={{ - paddingHorizontal: 12 - }} - > - {subscriptionType === SUBSCRIPTION_STATUS.TRIAL || - subscriptionType === SUBSCRIPTION_STATUS.BASIC ? ( - <MenuItem testID={pro.name} key={pro.name} item={pro} index={0} /> - ) : null} - </View> - - {dragging ? ( - <View - style={{ - paddingHorizontal: 12 - }} - > - <Button - type="secondaryAccented" - style={{ - flexDirection: "row", - borderRadius: 5, - marginBottom: 12, - justifyContent: "flex-start", - alignItems: "center", - paddingHorizontal: 12, - marginTop: 5, - paddingVertical: 6, - width: "100%" - }} - icon="close" - title={strings.stopReordering()} - onPress={() => { - useSideBarDraggingStore.setState({ - dragging: false - }); - }} - /> - </View> - ) : ( - <UserStatus /> - )} - </View> - </View> - ) : null; + <TabView + navigationState={{ index, routes }} + renderTabBar={(props) => <TabBar {...props} />} + tabBarPosition="bottom" + renderScene={renderScene} + onIndexChange={setIndex} + swipeEnabled={false} + animationEnabled={false} + /> + </SafeAreaView> + ); }, () => true ); + +const SettingsIcon = (props: { + route: Route; + isFocused: boolean; + jumpTo: (routeName: string) => void; +}) => { + const { colors } = useThemeColors(); + const userProfile = useUserStore((state) => state.profile); + const user = useUserStore((state) => state.user); + + return ( + <Pressable + key={props.route.key} + onPress={() => { + rootNavigatorRef.navigate("Settings"); + // if (!user) { + // rootNavigatorRef.navigate("Settings"); + // return; + // } + // UserSheet.present(); + }} + style={{ + borderRadius: 10, + paddingVertical: 2, + width: "25%" + }} + type={props.isFocused ? "selected" : "plain"} + > + {userProfile?.profilePicture ? ( + <Image + source={{ + uri: userProfile?.profilePicture + }} + style={{ + width: SIZE.lg, + height: SIZE.lg, + borderRadius: 100 + }} + /> + ) : ( + <Icon + name="cog-outline" + color={props.isFocused ? colors.primary.accent : colors.primary.icon} + size={SIZE.lg} + /> + )} + + <Paragraph + color={ + props.isFocused + ? colors.primary.paragraph + : colors.secondary.paragraph + } + style={{ + opacity: props.isFocused ? 1 : 0.6 + }} + size={SIZE.xxxs - 1} + > + {userProfile?.fullName + ? userProfile.fullName.split(" ")[0] + : props.route.title} + </Paragraph> + </Pressable> + ); +}; + +const TabBar = ( + props: SceneRendererProps & { + navigationState: NavigationState<Route>; + options: Record<string, TabDescriptor<Route>> | undefined; + } +) => { + const { colors } = useThemeColors(); + const notebookSelectionEnabled = useSideMenuNotebookSelectionStore( + (state) => state.enabled + ); + const tagSelectionEnabled = useSideMenuTagsSelectionStore( + (state) => state.enabled + ); + const isSelectionEnabled = notebookSelectionEnabled || tagSelectionEnabled; + + const getIcon = (key: string) => { + switch (key) { + case "home": + return "home-outline"; + case "settings": + return "cog-outline"; + case "notebooks": + return "book-outline"; + case "tags": + return "pound"; + default: + return "home-outline"; + } + }; + + return ( + <View + style={{ + flexDirection: "row", + width: "100%", + justifyContent: "space-between", + backgroundColor: colors.primary.background, + paddingHorizontal: DefaultAppStyles.GAP, + paddingVertical: DefaultAppStyles.GAP_SMALL + }} + > + {isSelectionEnabled ? ( + <> + {[ + { + title: "Select all", + icon: "check-all" + }, + { + title: "Delete", + icon: "delete" + }, + { + title: "Move", + icon: "arrow-right-bold-box-outline", + hidden: !notebookSelectionEnabled + }, + { + title: "Close", + icon: "close" + } + ].map((item) => ( + <> + <Pressable + key={item.title} + onPress={async () => { + switch (item.title) { + case "Select all": { + if (notebookSelectionEnabled) { + useSideMenuNotebookSelectionStore + .getState() + .selectAll?.(); + } else { + useSideMenuTagsSelectionStore.getState().selectAll?.(); + } + + break; + } + case "Delete": { + if (notebookSelectionEnabled) { + const ids = useSideMenuNotebookSelectionStore + .getState() + .getSelectedItemIds(); + deleteItems(ids, "notebook"); + } else { + const ids = useSideMenuTagsSelectionStore + .getState() + .getSelectedItemIds(); + deleteItems(ids, "tag"); + } + break; + } + case "Move": { + const ids = useSideMenuNotebookSelectionStore + .getState() + .getSelectedItemIds(); + const notebooks = await db.notebooks.all.items(ids); + MoveNotebookSheet.present(notebooks); + break; + } + case "Close": { + useSideMenuNotebookSelectionStore.setState({ + enabled: false, + selection: {} + }); + useSideMenuTagsSelectionStore.setState({ + enabled: false, + selection: {} + }); + break; + } + } + }} + style={{ + borderRadius: 10, + paddingVertical: 2, + width: "25%" + }} + type="plain" + > + <Icon + name={item.icon} + color={colors.primary.icon} + size={SIZE.lg} + /> + <Paragraph + color={colors.secondary.paragraph} + size={SIZE.xxxs - 1} + > + {item.title} + </Paragraph> + </Pressable> + </> + ))} + </> + ) : ( + <> + {props.navigationState.routes.map((route, index) => { + const isFocused = props.navigationState.index === index; + + return route.key === "settings" ? ( + <SettingsIcon + isFocused={isFocused} + jumpTo={props.jumpTo} + route={route} + /> + ) : ( + <Pressable + key={route.key} + onPress={() => { + props.jumpTo(route.key); + switch (route.key) { + case "notebooks": + Navigation.routeNeedsUpdate( + "Notebooks", + Navigation.routeUpdateFunctions.Notebooks + ); + break; + case "tags": + Navigation.routeNeedsUpdate( + "Tags", + Navigation.routeUpdateFunctions.Tags + ); + break; + default: + break; + } + }} + style={{ + borderRadius: 10, + opacity: isFocused ? 1 : 0.6, + paddingVertical: 2, + width: "25%" + }} + type={isFocused ? "selected" : "plain"} + > + <Icon + name={getIcon(route.key)} + color={ + isFocused ? colors.primary.accent : colors.primary.icon + } + size={SIZE.lg} + /> + <Paragraph + color={ + isFocused + ? colors.primary.paragraph + : colors.secondary.paragraph + } + size={SIZE.xxxs - 1} + > + {route.title} + </Paragraph> + </Pressable> + ); + })} + </> + )} + </View> + ); +}; diff --git a/apps/mobile/app/components/side-menu/menu-item.tsx b/apps/mobile/app/components/side-menu/menu-item.tsx index 82f2e7067..2590b927c 100644 --- a/apps/mobile/app/components/side-menu/menu-item.tsx +++ b/apps/mobile/app/components/side-menu/menu-item.tsx @@ -18,154 +18,191 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { useThemeColors } from "@notesnook/theme"; -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; import { View } from "react-native"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; -import ToggleSwitch from "toggle-switch-react-native"; +import { db } from "../../common/database"; +import { useTotalNotes } from "../../hooks/use-db-item"; import Navigation from "../../services/navigation"; -import useNavigationStore from "../../stores/use-navigation-store"; -import { SIZE, normalize } from "../../utils/size"; -import { Button } from "../ui/button"; +import { useFavoriteStore } from "../../stores/use-favorite-store"; +import useNavigationStore, { + RouteParams +} from "../../stores/use-navigation-store"; +import { useNoteStore } from "../../stores/use-notes-store"; +import { useTrashStore } from "../../stores/use-trash-store"; +import { SideMenuItem } from "../../utils/menu-items"; +import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; import { Pressable } from "../ui/pressable"; -import Heading from "../ui/typography/heading"; import Paragraph from "../ui/typography/paragraph"; -import { strings } from "@notesnook/intl"; function _MenuItem({ item, index, testID, - rightBtn + renderIcon }: { - item: any; - index: number; - testID: string; - rightBtn?: { - name: string; - icon: string; - func: () => void; - }; + item: SideMenuItem; + index?: number; + testID?: string; + renderIcon?: (item: SideMenuItem, size: number) => React.ReactNode; }) { + const [itemCount, setItemCount] = useState(0); const { colors } = useThemeColors(); const isFocused = useNavigationStore( - (state) => state.focusedRouteId === item.name + (state) => state.focusedRouteId === item.id ); - const primaryColors = isFocused ? colors.selected : colors.primary; + const totalNotes = useTotalNotes( + item.dataType as "notebook" | "tag" | "color" + ); + const getTotalNotesRef = useRef(totalNotes.getTotalNotes); + getTotalNotesRef.current = totalNotes.getTotalNotes; + + const menuItemCount = !item.data + ? itemCount + : totalNotes.totalNotes(item.data.id); + + useEffect(() => { + let unsub: () => void; + if (!item.data) { + switch (item.id) { + case "Notes": + unsub = useNoteStore.subscribe((state) => { + setItemCount( + useNoteStore.getState().items?.placeholders?.length || 0 + ); + }); + setItemCount( + useNoteStore.getState().items?.placeholders?.length || 0 + ); + break; + case "Favorites": + unsub = useFavoriteStore.subscribe((state) => { + setItemCount(state.items?.placeholders.length || 0); + }); + setItemCount( + useFavoriteStore.getState().items?.placeholders?.length || 0 + ); + break; + case "Reminders": + unsub = useFavoriteStore.subscribe((state) => { + setItemCount(state.items?.placeholders.length || 0); + }); + setItemCount( + useFavoriteStore.getState().items?.placeholders?.length || 0 + ); + break; + case "Monographs": + db.monographs.all.count().then((count) => { + setItemCount(count); + }); + // TODO make it reactive? + break; + case "Trash": + unsub = useTrashStore.subscribe((state) => { + setItemCount(state.items?.placeholders.length || 0); + }); + setItemCount( + useTrashStore.getState().items?.placeholders?.length || 0 + ); + break; + } + } else { + getTotalNotesRef.current?.([item.data.id]); + } + return () => { + unsub?.(); + }; + }, [item.data, item.id]); const _onPress = () => { - if (item.func) { - item.func(); - } else { - if (useNavigationStore.getState().currentRoute !== item.name) { - Navigation.navigate(item.name, { - canGoBack: false, - beta: item.isBeta - }); - } - } - if (item.close) { - setImmediate(() => { - Navigation.closeDrawer(); + if (item.onPress) return item.onPress(item); + + if (useNavigationStore.getState().currentRoute !== item.id) { + Navigation.navigate(item.id as keyof RouteParams, { + canGoBack: false }); } + setImmediate(() => { + Navigation.closeDrawer(); + }); }; return ( <Pressable testID={testID} - key={item.name + index} + key={item.id} onPress={_onPress} + onLongPress={() => item.onLongPress?.(item)} type={isFocused ? "selected" : "plain"} style={{ width: "100%", alignSelf: "center", borderRadius: 5, flexDirection: "row", - paddingHorizontal: 8, + paddingHorizontal: DefaultAppStyles.GAP_SMALL, justifyContent: "space-between", alignItems: "center", - height: normalize(50), - marginBottom: 5 + paddingVertical: DefaultAppStyles.GAP_VERTICAL }} > <View style={{ flexDirection: "row", - alignItems: "center" + alignItems: "center", + gap: DefaultAppStyles.GAP_SMALL }} > - <Icon - style={{ - width: 30, - textAlignVertical: "center", - textAlign: "left" - }} - allowFontScaling - name={item.icon} - color={ - item.icon === "crown" - ? colors.static.yellow - : isFocused - ? colors.selected.icon - : colors.secondary.icon - } - size={SIZE.lg - 2} - /> - {isFocused ? ( - <Heading color={colors.selected.heading} size={SIZE.md}> - {item.title || item.name} - </Heading> + {renderIcon ? ( + renderIcon(item, SIZE.md) ) : ( - <Paragraph size={SIZE.md}>{item.title || item.name}</Paragraph> + <Icon + style={{ + textAlignVertical: "center", + textAlign: "left" + }} + allowFontScaling + name={item.icon} + color={ + item.icon === "crown" + ? colors.static.yellow + : isFocused + ? colors.selected.paragraph + : colors.secondary.icon + } + size={SIZE.md} + /> )} - {item.isBeta ? ( - <View - style={{ - borderRadius: 100, - backgroundColor: primaryColors.accent, - paddingHorizontal: 4, - marginLeft: 5, - paddingVertical: 2 - }} - > - <Paragraph color={primaryColors.accentForeground} size={SIZE.xxs}> - {strings.beta()} - </Paragraph> - </View> - ) : null} + <Paragraph + color={ + isFocused ? colors.primary.paragraph : colors.secondary.paragraph + } + size={SIZE.sm} + > + {item.title} + </Paragraph> </View> - {item.switch ? ( - <ToggleSwitch - isOn={item.on} - onColor={primaryColors.accent} - offColor={primaryColors.icon} - size="small" - animationSpeed={150} - onToggle={_onPress} - /> - ) : rightBtn ? ( - <Button - title={rightBtn.name} - type="shade" - height={30} - fontSize={SIZE.xs} - iconSize={SIZE.xs} - icon={rightBtn.icon} - style={{ - borderRadius: 100, - paddingHorizontal: 16 - }} - onPress={rightBtn.func} - /> + {menuItemCount > 0 ? ( + <Paragraph + size={SIZE.xxs} + color={ + isFocused ? colors.primary.paragraph : colors.secondary.paragraph + } + > + {menuItemCount} + </Paragraph> ) : null} </Pressable> ); } export const MenuItem = React.memo(_MenuItem, (prev, next) => { - if (prev.item.name !== next.item.name) return false; - if (prev.rightBtn?.name !== next.rightBtn?.name) return false; + if ( + prev.item.id !== next.item.id && + prev.item.data?.dateModified !== next.item.data?.dateModified + ) + return false; return true; }); diff --git a/apps/mobile/app/components/side-menu/pinned-section.tsx b/apps/mobile/app/components/side-menu/pinned-section.tsx index 3db679b4b..f9342c680 100644 --- a/apps/mobile/app/components/side-menu/pinned-section.tsx +++ b/apps/mobile/app/components/side-menu/pinned-section.tsx @@ -18,30 +18,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { Notebook, Tag } from "@notesnook/core"; -import { useThemeColors } from "@notesnook/theme"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useMemo } from "react"; import { 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 Navigation from "../../services/navigation"; import { useMenuStore } from "../../stores/use-menu-store"; -import useNavigationStore from "../../stores/use-navigation-store"; import { useSettingStore } from "../../stores/use-setting-store"; -import { SIZE, normalize } from "../../utils/size"; +import { SideMenuItem } from "../../utils/menu-items"; import ReorderableList from "../list/reorderable-list"; -import { Button } from "../ui/button"; -import { Notice } from "../ui/notice"; -import { Pressable } from "../ui/pressable"; -import Seperator from "../ui/seperator"; -import SheetWrapper from "../ui/sheet"; -import Heading from "../ui/typography/heading"; -import Paragraph from "../ui/typography/paragraph"; -import { strings } from "@notesnook/intl"; +import { MenuItem } from "./menu-item"; +import { useThemeColors } from "@notesnook/theme"; +import { DefaultAppStyles } from "../../utils/styles"; export const PinnedSection = React.memo( function PinnedSection() { + const { colors } = useThemeColors(); const menuPins = useMenuStore((state) => state.menuPins); const loading = useSettingStore((state) => state.isAppLoading); const setMenuPins = useMenuStore((state) => state.setMenuPins); @@ -52,42 +45,55 @@ export const PinnedSection = React.memo( } }, [loading, setMenuPins]); - const onPress = (item: Notebook | Tag) => { - if (item.type === "notebook") { - NotebookScreen.navigate(item); - } else if (item.type === "tag") { - TaggedNotes.navigate(item); + const onPress = React.useCallback((item: SideMenuItem) => { + const data = item.data as Notebook | Tag; + if (data.type === "notebook") { + NotebookScreen.navigate(data); + } else if (data.type === "tag") { + TaggedNotes.navigate(data); } setImmediate(() => { Navigation.closeDrawer(); }); - }; - const renderItem = ({ - item, - index - }: { - item: Notebook | Tag; - index: number; - }) => { - return <PinItem item={item} onPress={onPress} />; - }; + }, []); + + const menuItems = useMemo( + () => + menuPins.map((item) => ({ + id: item.id, + title: item.title, + icon: item.type === "notebook" ? "notebook-outline" : "pound", + dataType: item.type, + data: item, + onPress: onPress + })) as SideMenuItem[], + [menuPins, onPress] + ); + + const renderItem = React.useCallback(({ item }: { item: SideMenuItem }) => { + return <MenuItem item={item} />; + }, []); return ( <View style={{ - flexGrow: 1 + flexGrow: 1, + borderTopWidth: 1, + borderTopColor: colors.primary.border, + marginTop: DefaultAppStyles.GAP_SMALL, + paddingTop: DefaultAppStyles.GAP_SMALL }} > <ReorderableList onListOrderChanged={(data) => { db.settings.setSideBarOrder("shortcuts", data); }} - onHiddenItemsChanged={(data) => {}} + onHiddenItemsChanged={() => {}} canHideItems={false} itemOrder={order} hiddenItems={[]} alwaysBounceVertical={false} - data={menuPins} + data={menuItems} style={{ flexGrow: 1, width: "100%" @@ -97,161 +103,9 @@ export const PinnedSection = React.memo( }} showsVerticalScrollIndicator={false} renderDraggableItem={renderItem} - ListEmptyComponent={ - <Notice - size="small" - type="information" - text={strings.sideMenuNotice()} - style={{ - marginHorizontal: 12 - }} - /> - } /> </View> ); }, () => true ); - -export const PinItem = React.memo( - function PinItem({ - item, - onPress, - isPlaceholder - }: { - item: Notebook | Tag; - onPress: (item: Notebook | Tag) => void; - isPlaceholder?: boolean; - }) { - const { colors } = useThemeColors(); - const setMenuPins = useMenuStore((state) => state.setMenuPins); - - const [visible, setVisible] = useState(false); - const isFocused = useNavigationStore( - (state) => state.focusedRouteId === item.id - ); - const primaryColors = isFocused ? colors.selected : colors.primary; - const color = isFocused ? colors.selected.accent : colors.primary.icon; - const fwdRef = useRef(); - - const icons = { - topic: "bookmark", - notebook: "book-outline", - tag: "pound" - }; - - return ( - <> - {visible && ( - <SheetWrapper - onClose={() => { - setVisible(false); - }} - gestureEnabled={false} - fwdRef={fwdRef} - > - <Seperator /> - <Button - title={strings.removeShortcut()} - type="error" - onPress={async () => { - await db.shortcuts.remove(item.id); - setVisible(false); - setMenuPins(); - }} - fontSize={SIZE.md} - width="95%" - style={{ - marginBottom: 30 - }} - /> - </SheetWrapper> - )} - <Pressable - type={isFocused ? "selected" : "plain"} - onPress={() => onPress(item)} - style={{ - width: "100%", - alignSelf: "center", - borderRadius: 5, - flexDirection: "row", - paddingHorizontal: 8, - justifyContent: "space-between", - alignItems: "center", - height: normalize(50), - marginBottom: 5 - }} - > - <View - style={{ - flexDirection: "row", - alignItems: "center", - flexGrow: 1, - flex: 1 - }} - > - <View - style={{ - width: 30, - justifyContent: "center" - }} - > - <Icon - allowFontScaling - color={color} - size={SIZE.lg - 2} - name={icons[item.type]} - /> - <Icon - style={{ - position: "absolute", - bottom: -6, - left: -6 - }} - allowFontScaling - color={color} - size={SIZE.xs} - name="arrow-top-right-thick" - /> - </View> - <View - style={{ - alignItems: "flex-start", - flexGrow: 1, - flex: 1 - }} - > - {isFocused ? ( - <Heading - style={{ - flexWrap: "wrap" - }} - color={primaryColors.heading} - size={SIZE.md} - > - {item.title} - </Heading> - ) : ( - <Paragraph - numberOfLines={1} - color={primaryColors.paragraph} - size={SIZE.md} - > - {item.title} - </Paragraph> - )} - </View> - </View> - </Pressable> - </> - ); - }, - (prev, next) => { - if (!next.item) return false; - if (prev.item.title !== next.item.title) return false; - if (prev.item?.dateModified !== next.item?.dateModified) return false; - if (prev.item?.id !== next.item?.id) return false; - return true; - } -); diff --git a/apps/mobile/app/components/side-menu/side-menu-header.tsx b/apps/mobile/app/components/side-menu/side-menu-header.tsx new file mode 100644 index 000000000..316b5c36b --- /dev/null +++ b/apps/mobile/app/components/side-menu/side-menu-header.tsx @@ -0,0 +1,112 @@ +/* +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 from "react"; +import { View } from "react-native"; +import { useThemeStore } from "../../stores/use-theme-store"; +import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; +import { IconButton, IconButtonProps } from "../ui/icon-button"; +import { SvgView } from "../ui/svg"; +import Heading from "../ui/typography/heading"; + +export const SideMenuHeader = (props: { rightButtons?: IconButtonProps[] }) => { + const { colors, isDark } = useThemeColors(); + return ( + <View + style={{ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: DefaultAppStyles.GAP + }} + > + <View + style={{ + flexDirection: "row", + gap: DefaultAppStyles.GAP_SMALL, + alignItems: "center" + }} + > + <View + style={{ + backgroundColor: "black", + width: 28, + height: 28, + borderRadius: 10 + }} + > + <SvgView + width={28} + height={28} + src={`<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_24_33)"> +<path d="M1024 0H0V1024H1024V0Z" fill="black"/> +<path d="M724.985 682.919C707.73 733.33 673.15 775.984 627.397 803.291C581.645 830.598 527.687 840.787 475.128 832.044C422.568 823.301 374.814 796.194 340.365 755.546C305.916 714.898 287.006 663.347 287 610.064V499.814L366.121 532.867V610.019C366.114 630.798 370.555 651.337 379.145 670.256C387.735 689.176 400.276 706.037 415.925 719.707C418.895 722.294 421.978 724.814 425.161 727.166C448.518 744.554 476.563 754.518 505.655 755.763C506.645 755.763 507.601 755.842 508.58 755.864C509.559 755.887 510.83 755.864 511.955 755.864C513.08 755.864 514.205 755.864 515.33 755.864C516.455 755.864 517.265 755.864 518.255 755.763C547.336 754.515 575.371 744.56 598.726 727.188C601.899 724.837 604.981 722.328 607.963 719.741C628.519 701.761 643.619 678.375 651.545 652.241L724.985 682.919Z" fill="white"/> +<path d="M737 414V610.065C737 612.596 737 615.139 736.842 617.67L657.879 584.651V414C657.866 376.316 643.272 340.099 617.154 312.934C591.035 285.77 555.419 269.766 517.765 268.274C480.11 266.782 443.339 279.918 415.154 304.931C386.968 329.944 369.554 364.893 366.56 402.457C366.279 406.26 366.121 410.119 366.121 414V462.712L287 429.637V189H512C571.674 189 628.903 212.705 671.099 254.901C713.295 297.097 737 354.326 737 414Z" fill="white"/> +</g> +<defs> +<clipPath id="clip0_24_33"> +<rect width="1024" height="1024" rx="200" fill="white"/> +</clipPath> +</defs> +</svg> +`} + /> + </View> + + <Heading size={SIZE.lg}>Notesnook</Heading> + </View> + + <View + style={{ + flexDirection: "row", + alignItems: "center", + gap: DefaultAppStyles.GAP_SMALL + }} + > + {props.rightButtons?.map((button, index) => ( + <IconButton + key={index} + {...button} + style={{ + width: 28, + height: 28 + }} + color={colors.primary.icon} + size={SIZE.lg} + /> + ))} + + <IconButton + onPress={() => { + useThemeStore.getState().setColorScheme(); + }} + style={{ + width: 28, + height: 28 + }} + color={colors.primary.icon} + name={isDark ? "weather-night" : "weather-sunny"} + size={SIZE.lg} + /> + </View> + </View> + ); +}; diff --git a/apps/mobile/app/components/side-menu/side-menu-home.tsx b/apps/mobile/app/components/side-menu/side-menu-home.tsx new file mode 100644 index 000000000..10c3eab4f --- /dev/null +++ b/apps/mobile/app/components/side-menu/side-menu-home.tsx @@ -0,0 +1,105 @@ +/* +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 { strings } from "@notesnook/intl"; +import { useThemeColors } from "@notesnook/theme"; +import React from "react"; +import { View } from "react-native"; +import { DraxProvider, DraxScrollView } from "react-native-drax"; +import { db } from "../../common/database"; +import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; +import { useMenuStore } from "../../stores/use-menu-store"; +import { useSettingStore } from "../../stores/use-setting-store"; +import { MenuItemsList } from "../../utils/menu-items"; +import { DefaultAppStyles } from "../../utils/styles"; +import ReorderableList from "../list/reorderable-list"; +import { ColorSection } from "./color-section"; +import { MenuItem } from "./menu-item"; +import { PinnedSection } from "./pinned-section"; +import { SideMenuHeader } from "./side-menu-header"; + +export function SideMenuHome() { + const { colors } = useThemeColors(); + const [isAppLoading, introCompleted] = useSettingStore((state) => [ + state.isAppLoading, + state.settings.introCompleted + ]); + const [order, hiddensItems] = useMenuStore((state) => [ + state.order["routes"], + state.hiddenItems["routes"] + ]); + + return ( + <View + style={{ + height: "100%", + width: "100%", + paddingTop: DefaultAppStyles.GAP_SMALL, + backgroundColor: colors.primary.background, + gap: DefaultAppStyles.GAP, + paddingHorizontal: DefaultAppStyles.GAP + }} + > + <SideMenuHeader /> + + {!isAppLoading && introCompleted ? ( + <DraxProvider> + <DraxScrollView nestedScrollEnabled={false}> + <ReorderableList + onListOrderChanged={(data) => { + db.settings.setSideBarOrder("routes", data); + }} + onHiddenItemsChanged={(data) => { + db.settings.setSideBarHiddenItems("routes", data); + }} + itemOrder={order} + hiddenItems={hiddensItems} + alwaysBounceVertical={false} + data={MenuItemsList} + style={{ + width: "100%" + }} + contentContainerStyle={{ + gap: 2 + }} + showsVerticalScrollIndicator={false} + renderDraggableItem={({ item, index }) => { + return ( + <MenuItem + key={item.title} + item={{ + ...item, + title: + strings.routes[ + item.title as keyof typeof strings.routes + ]?.() || item.title + }} + testID={item.title} + index={index} + /> + ); + }} + /> + <ColorSection /> + <PinnedSection /> + </DraxScrollView> + </DraxProvider> + ) : null} + </View> + ); +} diff --git a/apps/mobile/app/components/side-menu/side-menu-notebooks.tsx b/apps/mobile/app/components/side-menu/side-menu-notebooks.tsx new file mode 100644 index 000000000..2850782de --- /dev/null +++ b/apps/mobile/app/components/side-menu/side-menu-notebooks.tsx @@ -0,0 +1,450 @@ +/* +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 } from "@notesnook/core"; +import { useThemeColors } from "@notesnook/theme"; +import React, { useEffect } from "react"; +import { FlatList, ListRenderItemInfo, View } from "react-native"; +import { UseBoundStore } from "zustand"; +import { db } from "../../common/database"; +import { useTotalNotes } from "../../hooks/use-db-item"; +import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; +import NotebookScreen from "../../screens/notebook"; +import { + eSubscribeEvent, + eUnSubscribeEvent +} from "../../services/event-manager"; +import { TreeItem } from "../../stores/create-notebook-tree-stores"; +import { SelectionStore } from "../../stores/item-selection-store"; +import useNavigationStore from "../../stores/use-navigation-store"; +import { useNotebooks } from "../../stores/use-notebook-store"; +import { eOnNotebookUpdated } from "../../utils/events"; +import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; +import { Properties } from "../properties"; +import { AddNotebookSheet } from "../sheets/add-notebook"; +import AppIcon from "../ui/AppIcon"; +import { IconButton } from "../ui/icon-button"; +import { Pressable } from "../ui/pressable"; +import Paragraph from "../ui/typography/paragraph"; +import { SideMenuHeader } from "./side-menu-header"; +import { + useSideMenuNotebookExpandedStore, + useSideMenuNotebookSelectionStore, + useSideMenuNotebookTreeStore +} from "./stores"; + +const NotebookItem = ({ + index, + item, + expanded, + selected, + onToggleExpanded, + focused, + selectionEnabled, + selectionStore, + onItemUpdate, + onPress, + onLongPress +}: { + index: number; + item: TreeItem; + expanded?: boolean; + onToggleExpanded?: () => void; + selected?: boolean; + focused?: boolean; + selectionEnabled?: boolean; + selectionStore: UseBoundStore<SelectionStore>; + onItemUpdate: (id?: string) => void; + onPress?: () => void; + onLongPress?: () => void; +}) => { + const notebook = item.notebook; + const [nestedNotebooksSelected, setNestedNotebooksSelected] = + React.useState(false); + const isFocused = focused; + const { totalNotes, getTotalNotes } = useTotalNotes("notebook"); + const getTotalNotesRef = React.useRef(getTotalNotes); + getTotalNotesRef.current = getTotalNotes; + const { colors } = useThemeColors("sheet"); + + useEffect(() => { + getTotalNotesRef.current([item.notebook.id]); + }, [item.notebook]); + + useEffect(() => { + if (selectionEnabled) { + const selector = db.relations.from( + { + type: "notebook", + id: item.notebook.id + }, + "notebook" + ).selector; + selector.ids().then((ids) => { + setNestedNotebooksSelected( + ids.length === 0 + ? true + : ids.every( + (id) => selectionStore.getState().selection[id] === "selected" + ) + ); + }); + } + }, [selected, item.notebook.id, selectionEnabled, selectionStore]); + + async function selectAll() { + const selector = db.relations.from( + { + type: "notebook", + id: item.notebook.id + }, + "notebook" + ).selector; + const ids = await selector.ids(); + selectionStore.setState({ + selection: { + ...selectionStore.getState().selection, + ...ids.reduce((acc: any, id) => { + acc[id] = "selected"; + return acc; + }, {}) + } + }); + setNestedNotebooksSelected(true); + } + + async function deselectAll() { + const selector = db.relations.from( + { + type: "notebook", + id: item.notebook.id + }, + "notebook" + ).selector; + const ids = await selector.ids(); + useSideMenuNotebookSelectionStore.setState({ + selection: { + ...selectionStore.getState().selection, + ...ids.reduce((acc: any, id) => { + acc[id] = "deselected"; + return acc; + }, {}) + } + }); + setNestedNotebooksSelected(false); + } + + useEffect(() => { + const unsub = selectionStore.subscribe((state) => { + if (state.enabled) { + const selector = db.relations.from( + { + type: "notebook", + id: item.notebook.id + }, + "notebook" + ).selector; + selector.ids().then((ids) => { + if (!ids.length) return; + setNestedNotebooksSelected( + ids.length === 0 + ? true + : ids.every( + (id) => selectionStore.getState().selection[id] === "selected" + ) + ); + }); + } + }); + + return () => { + unsub(); + }; + }, [item.notebook.id, selectionStore]); + + useEffect(() => { + const onNotebookUpdate = (id?: string) => { + if (id && id !== notebook.id) return; + onItemUpdate(id); + }; + + eSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate); + return () => { + eUnSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate); + }; + }, [notebook.id, onItemUpdate]); + + return ( + <View + style={{ + paddingLeft: item.depth > 0 && item.depth < 6 ? 15 : undefined, + width: "100%", + marginTop: 2 + }} + > + <Pressable + type={isFocused ? "selected" : "transparent"} + onLongPress={() => { + onLongPress?.(); + }} + testID={`notebook-sheet-item-${item.depth}-${index}`} + onPress={async () => { + if (selectionEnabled) { + if (selected && !nestedNotebooksSelected) { + console.log("Select all..."); + return selectAll(); + } + await deselectAll(); + selectionStore + .getState() + .markAs(item.notebook, selected ? "deselected" : "selected"); + if (selectionStore.getState().getSelectedItemIds().length === 0) { + selectionStore.setState({ + enabled: false + }); + } + } else { + onPress?.(); + } + }} + style={{ + justifyContent: "space-between", + width: "100%", + alignItems: "center", + flexDirection: "row", + borderRadius: 5, + paddingRight: DefaultAppStyles.GAP_SMALL + }} + > + <View + style={{ + flexDirection: "row", + alignItems: "center" + }} + > + <IconButton + size={SIZE.md} + color={selected ? colors.selected.icon : colors.primary.icon} + onPress={() => { + onToggleExpanded?.(); + }} + top={0} + left={0} + bottom={0} + right={0} + style={{ + width: 32, + height: 32, + borderRadius: 5 + }} + name={expanded ? "chevron-down" : "chevron-right"} + /> + + <Paragraph + color={ + isFocused ? colors.selected.paragraph : colors.secondary.paragraph + } + size={SIZE.xs} + > + {notebook?.title} + </Paragraph> + </View> + + {selectionEnabled ? ( + <View + style={{ + width: 22, + height: 22, + justifyContent: "center", + alignItems: "center" + }} + > + <AppIcon + name={ + selected + ? !nestedNotebooksSelected + ? "checkbox-intermediate" + : "checkbox-outline" + : "checkbox-blank-outline" + } + color={selected ? colors.selected.icon : colors.primary.icon} + /> + </View> + ) : ( + <> + {totalNotes(notebook?.id) ? ( + <Paragraph size={SIZE.xxs} color={colors.secondary.paragraph}> + {totalNotes?.(notebook?.id)} + </Paragraph> + ) : null} + </> + )} + </Pressable> + </View> + ); +}; + +export const SideMenuNotebooks = () => { + const tree = useSideMenuNotebookTreeStore((state) => state.tree); + const [notebooks, loading] = useNotebooks(); + const insets = useGlobalSafeAreaInsets(); + + const loadRootNotebooks = React.useCallback(async () => { + if (!notebooks) return; + const _notebooks: Notebook[] = []; + for (let i = 0; i < notebooks.placeholders.length; i++) { + _notebooks[i] = (await notebooks?.item(i))?.item as Notebook; + } + useSideMenuNotebookTreeStore.getState().addNotebooks("root", _notebooks, 0); + }, [notebooks]); + + useEffect(() => { + (async () => { + if (!loading) { + loadRootNotebooks(); + } + })(); + }, [loadRootNotebooks, loading]); + + useEffect(() => { + useSideMenuNotebookSelectionStore.setState({ + selectAll: async () => { + const allNotebooks = await db.notebooks.all.items(); + const allSelected = allNotebooks.every((notebook) => { + return ( + useSideMenuNotebookSelectionStore.getState().selection[ + notebook.id + ] === "selected" + ); + }); + + if (allSelected) { + useSideMenuNotebookSelectionStore.setState({ + selection: {} + }); + return; + } + + useSideMenuNotebookSelectionStore.setState({ + selection: allNotebooks.reduce((acc: any, item) => { + acc[item.id] = "selected"; + return acc; + }, {}) + }); + } + }); + }, []); + + const renderItem = React.useCallback((info: ListRenderItemInfo<TreeItem>) => { + return <NotebookItemWrapper index={info.index} item={info.item} />; + }, []); + + return ( + <FlatList + style={{ + paddingHorizontal: DefaultAppStyles.GAP, + paddingTop: DefaultAppStyles.GAP_SMALL + }} + data={tree} + keyExtractor={(item) => item.notebook.id} + windowSize={3} + ListHeaderComponent={ + <SideMenuHeader + rightButtons={[ + { + name: "plus", + onPress: () => { + AddNotebookSheet.present(); + } + } + ]} + /> + } + stickyHeaderIndices={[0]} + renderItem={renderItem} + /> + ); +}; + +const NotebookItemWrapper = ({ + item, + index +}: { + item: TreeItem; + index: number; +}) => { + const expanded = useSideMenuNotebookExpandedStore( + (state) => state.expanded[item.notebook.id] + ); + const selectionEnabled = useSideMenuNotebookSelectionStore( + (state) => state.enabled + ); + const selected = useSideMenuNotebookSelectionStore( + (state) => state.selection[item.notebook.id] === "selected" + ); + const focused = useNavigationStore( + (state) => state.focusedRouteId === item.notebook.id + ); + + useEffect(() => { + if (expanded) { + useSideMenuNotebookTreeStore + .getState() + .fetchAndAdd(item.notebook.id, item.depth + 1); + } else { + useSideMenuNotebookTreeStore.getState().removeChildren(item.notebook.id); + } + }, [expanded, item.depth, item.notebook]); + + const onItemUpdate = React.useCallback(async () => { + const notebook = await db.notebooks.notebook(item.notebook.id); + if (notebook) { + useSideMenuNotebookTreeStore + .getState() + .updateItem(item.notebook.id, notebook); + } else { + useSideMenuNotebookTreeStore.getState().removeItem(item.notebook.id); + } + }, [item.notebook.id]); + + return ( + <NotebookItem + item={item} + index={index} + expanded={expanded} + onToggleExpanded={() => { + useSideMenuNotebookExpandedStore + .getState() + .setExpanded(item.notebook.id); + }} + selected={selected} + selectionEnabled={selectionEnabled} + selectionStore={useSideMenuNotebookSelectionStore} + onItemUpdate={onItemUpdate} + focused={focused} + onPress={() => { + NotebookScreen.navigate(item.notebook, false); + }} + onLongPress={() => { + Properties.present(item.notebook, false); + }} + /> + ); +}; diff --git a/apps/mobile/app/components/side-menu/side-menu-tags.tsx b/apps/mobile/app/components/side-menu/side-menu-tags.tsx new file mode 100644 index 000000000..08dc407d5 --- /dev/null +++ b/apps/mobile/app/components/side-menu/side-menu-tags.tsx @@ -0,0 +1,207 @@ +/* +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 { Tag, VirtualizedGrouping } from "@notesnook/core"; +import { useThemeColors } from "@notesnook/theme"; +import React, { useEffect } from "react"; +import { View } from "react-native"; +import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList"; +import { db } from "../../common/database"; +import { useDBItem, useTotalNotes } from "../../hooks/use-db-item"; +import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; +import { TaggedNotes } from "../../screens/notes/tagged"; +import useNavigationStore from "../../stores/use-navigation-store"; +import { useTags } from "../../stores/use-tag-store"; +import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; +import { Properties } from "../properties"; +import AppIcon from "../ui/AppIcon"; +import { Pressable } from "../ui/pressable"; +import Paragraph from "../ui/typography/paragraph"; +import { SideMenuHeader } from "./side-menu-header"; +import { useSideMenuTagsSelectionStore } from "./stores"; + +const TagItem = (props: { + tags: VirtualizedGrouping<Tag>; + id: number | string; +}) => { + const { colors } = useThemeColors(); + const [item] = useDBItem(props.id, "tag", props.tags); + const isSelected = useSideMenuTagsSelectionStore((state) => + item?.id ? state.selection[item.id] === "selected" : false + ); + const enabled = useSideMenuTagsSelectionStore((state) => state.enabled); + const isFocused = useNavigationStore( + (state) => state.focusedRouteId === item?.id + ); + const totalNotes = useTotalNotes("tag"); + const totalNotesRef = React.useRef(totalNotes); + totalNotesRef.current = totalNotes; + + useEffect(() => { + if (item?.id) { + totalNotesRef.current?.getTotalNotes([item?.id]); + } + }, [item]); + + return item ? ( + <Pressable + type={isSelected || isFocused ? "selected" : "transparent"} + onLongPress={() => { + Properties.present(item); + }} + testID={`tag-item-${props.id}`} + onPress={() => { + if (enabled) { + useSideMenuTagsSelectionStore + .getState() + .markAs(item, isSelected ? "deselected" : "selected"); + if ( + useSideMenuTagsSelectionStore.getState().getSelectedItemIds() + .length === 0 + ) { + useSideMenuTagsSelectionStore.setState({ + enabled: false + }); + } + } else { + TaggedNotes.navigate(item, false); + } + }} + style={{ + justifyContent: "space-between", + width: "100%", + alignItems: "center", + flexDirection: "row", + borderRadius: 5, + paddingRight: DefaultAppStyles.GAP_SMALL + }} + > + <View + style={{ + flexDirection: "row", + alignItems: "center" + }} + > + <View + style={{ + width: 32, + height: 32, + justifyContent: "center", + alignItems: "center" + }} + > + <AppIcon + size={SIZE.md} + color={isFocused ? colors.selected.icon : colors.secondary.icon} + name="pound" + /> + </View> + + <Paragraph + color={ + isFocused ? colors.selected.paragraph : colors.secondary.paragraph + } + size={SIZE.xs} + > + {item?.title} + </Paragraph> + </View> + + {enabled ? ( + <View + style={{ + width: 22, + height: 22, + justifyContent: "center", + alignItems: "center" + }} + > + <AppIcon + name={isSelected ? "checkbox-outline" : "checkbox-blank-outline"} + color={isSelected ? colors.selected.icon : colors.primary.icon} + /> + </View> + ) : ( + <> + {item?.id && totalNotes.totalNotes?.(item?.id) ? ( + <Paragraph size={SIZE.xxs} color={colors.secondary.paragraph}> + {totalNotes.totalNotes(item?.id)} + </Paragraph> + ) : null} + </> + )} + </Pressable> + ) : null; +}; + +export const SideMenuTags = () => { + const [tags] = useTags(); + const insets = useGlobalSafeAreaInsets(); + + useEffect(() => { + useSideMenuTagsSelectionStore.setState({ + selectAll: async () => { + const tags = await db.tags.all.items(); + const allSelected = tags.every((tag) => { + return ( + useSideMenuTagsSelectionStore.getState().selection[tag.id] === + "selected" + ); + }); + + if (allSelected) { + useSideMenuTagsSelectionStore.getState().setSelection({}); + } else { + useSideMenuTagsSelectionStore.getState().setSelection( + tags.reduce((acc: any, tag) => { + acc[tag.id] = "selected"; + return acc; + }, {}) + ); + } + } + }); + }, []); + + const renderItem = React.useCallback( + (info: { index: number }) => { + return <TagItem id={info.index} tags={tags!} />; + }, + [tags] + ); + + return ( + <View + style={{ + paddingHorizontal: DefaultAppStyles.GAP, + paddingTop: DefaultAppStyles.GAP_SMALL, + width: "100%", + height: "100%" + }} + > + <SideMenuHeader /> + <FlashList + data={tags?.placeholders} + estimatedItemSize={32} + renderItem={renderItem} + /> + </View> + ); +}; diff --git a/apps/mobile/app/components/side-menu/stores.ts b/apps/mobile/app/components/side-menu/stores.ts new file mode 100644 index 000000000..7b06fde42 --- /dev/null +++ b/apps/mobile/app/components/side-menu/stores.ts @@ -0,0 +1,32 @@ +/* +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 { createNotebookTreeStores } from "../../stores/create-notebook-tree-stores"; +import { createItemSelectionStore } from "../../stores/item-selection-store"; + +export const { + useSideMenuNotebookExpandedStore, + useSideMenuNotebookSelectionStore, + useSideMenuNotebookTreeStore +} = createNotebookTreeStores(true, false); + +export const useSideMenuTagsSelectionStore = createItemSelectionStore( + true, + false +); diff --git a/apps/mobile/app/components/side-menu/user-status.js b/apps/mobile/app/components/side-menu/user-status.js index d24e808a2..4291df027 100644 --- a/apps/mobile/app/components/side-menu/user-status.js +++ b/apps/mobile/app/components/side-menu/user-status.js @@ -17,6 +17,7 @@ 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 { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; import { useNetInfo } from "@react-native-community/netinfo"; import React from "react"; @@ -30,13 +31,12 @@ import Sync from "../../services/sync"; import { useThemeStore } from "../../stores/use-theme-store"; import { SyncStatus, useUserStore } from "../../stores/use-user-store"; import { eOpenLoginDialog } from "../../utils/events"; -import { tabBarRef } from "../../utils/global-refs"; +import { fluidTabsRef } from "../../utils/global-refs"; import { SIZE } from "../../utils/size"; import { IconButton } from "../ui/icon-button"; import { Pressable } from "../ui/pressable"; import { TimeSince } from "../ui/time-since"; import Paragraph from "../ui/typography/paragraph"; -import { strings } from "@notesnook/intl"; export const UserStatus = () => { const { colors, isDark } = useThemeColors(); @@ -71,7 +71,7 @@ export const UserStatus = () => { <Pressable onPress={() => { Navigation.navigate("Settings"); - tabBarRef.current.closeDrawer(); + fluidTabsRef.current.closeDrawer(); }} type="plain" style={{ @@ -206,7 +206,7 @@ export const UserStatus = () => { if (user) { Sync.run(); } else { - tabBarRef.current?.closeDrawer(); + fluidTabsRef.current?.closeDrawer(); eSendEvent(eOpenLoginDialog); } }} diff --git a/apps/mobile/app/components/ui/icon-button/index.tsx b/apps/mobile/app/components/ui/icon-button/index.tsx index 0b0c4dd7a..48eb0ae4e 100644 --- a/apps/mobile/app/components/ui/icon-button/index.tsx +++ b/apps/mobile/app/components/ui/icon-button/index.tsx @@ -25,7 +25,7 @@ import { RGB_Linear_Shade, hexToRGBA } from "../../../utils/colors"; import { SIZE } from "../../../utils/size"; import NativeTooltip from "../../../utils/tooltip"; import { Pressable, PressableProps } from "../pressable"; -interface IconButtonProps extends PressableProps { +export interface IconButtonProps extends PressableProps { name: string; color?: ColorValue; size?: number; diff --git a/apps/mobile/app/components/ui/pressable/index.tsx b/apps/mobile/app/components/ui/pressable/index.tsx index b948c77bd..b7f346925 100644 --- a/apps/mobile/app/components/ui/pressable/index.tsx +++ b/apps/mobile/app/components/ui/pressable/index.tsx @@ -273,13 +273,12 @@ export const Pressable = ({ justifyContent: "center", alignItems: "center", marginBottom: 0, - borderColor: - pressed && !disabled - ? customSelectedColor - ? getColorLinearShade(customSelectedColor, 0.3, false) - : borderSelectedColor || borderColor - : borderColor || "transparent", - borderWidth: borderWidth + borderColor: pressed + ? customSelectedColor + ? getColorLinearShade(customSelectedColor, 0.3, false) + : borderSelectedColor || borderColor + : borderColor || "transparent", + borderWidth: noborder ? 0 : borderWidth }, style, { diff --git a/apps/mobile/app/hooks/use-actions.tsx b/apps/mobile/app/hooks/use-actions.tsx index 4bd18f34b..336cf65a8 100644 --- a/apps/mobile/app/hooks/use-actions.tsx +++ b/apps/mobile/app/hooks/use-actions.tsx @@ -18,20 +18,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* eslint-disable no-inner-declarations */ import { - Color, - createInternalLink, + Item, ItemReference, Note, - Notebook, - Reminder, - Tag, - TrashItem, - VAULT_ERRORS + VAULT_ERRORS, + createInternalLink } from "@notesnook/core"; import { strings } from "@notesnook/intl"; +import { useThemeColors } from "@notesnook/theme"; import { DisplayedNotification } from "@notifee/react-native"; import Clipboard from "@react-native-clipboard/clipboard"; -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { InteractionManager, Platform } from "react-native"; import Share from "react-native-share"; import { DatabaseLogger, db } from "../common/database"; @@ -48,6 +45,11 @@ import { ReferencesList } from "../components/sheets/references"; import { RelationsList } from "../components/sheets/relations-list/index"; import ReminderSheet from "../components/sheets/reminder"; import { useSideBarDraggingStore } from "../components/side-menu/dragging-store"; +import { + useSideMenuNotebookSelectionStore, + useSideMenuTagsSelectionStore +} from "../components/side-menu/stores"; +import { ButtonProps } from "../components/ui/button"; import { useTabStore } from "../screens/editor/tiptap/use-tab-store"; import { eSendEvent, @@ -69,13 +71,83 @@ import { deleteItems } from "../utils/functions"; import { convertNoteToText } from "../utils/note-to-text"; import { sleep } from "../utils/time"; +export type ActionId = + | "select" + | "restore" + | "delete" + | "reorder" + | "rename-tag" + | "rename-color" + | "pin" + | "add-shortcut" + | "rename-notebook" + | "add-notebook" + | "edit-notebook" + | "default-notebook" + | "move-notes" + | "move-notebook" + | "disable-reminder" + | "edit-reminder" + | "delete-reminder" + | "delete" + | "delete-trash" + | "add-reminder" + | "copy" + | "share" + | "read-only" + | "local-only" + | "duplicate" + | "add-note" + | "attachments" + | "history" + | "copy-link" + | "reminders" + | "lock-unlock" + | "publish" + | "export" + | "notebooks" + | "add-tag" + | "references" + | "pin-to-notifications" + | "favorite" + | "remove-from-notebook" + | "trash"; + +export type Action = { + id: ActionId; + title: string; + icon: string; + onPress: () => void; + isToggle?: boolean; + checked?: boolean; + pro?: boolean; + hidden?: boolean; + activeColor?: string; + type?: ButtonProps["type"]; +}; + +function isNotePinnedInNotifications(item: Item) { + const pinned = Notifications.getPinnedNotes(); + if (!pinned || pinned.length === 0) { + return undefined; + } + const index = pinned.findIndex((notif) => notif.id === item.id); + if (index !== -1) { + return pinned[index]; + } + return undefined; +} + export const useActions = ({ close, - item + item, + customActionHandlers }: { - item: Note | Notebook | Reminder | Tag | Color | TrashItem; + item: Item; close: () => void; + customActionHandlers?: Record<ActionId, () => void>; }) => { + const { colors } = useThemeColors(); const setMenuPins = useMenuStore((state) => state.setMenuPins); const [isPinnedToMenu, setIsPinnedToMenu] = useState( db.shortcuts.exists(item.id) @@ -83,15 +155,14 @@ export const useActions = ({ const processingId = useRef<"shareNote" | "copyContent">(); const user = useUserStore((state) => state.user); const [notifPinned, setNotifPinned] = useState<DisplayedNotification>(); - const [defaultNotebook, setDefaultNotebook] = useState( db.settings.getDefaultNotebook() ); const [noteInCurrentNotebook, setNoteInCurrentNotebook] = useState(false); + const [locked, setLocked] = useState(false); const isPublished = item.type === "note" && db.monographs.isPublished(item.id); - const [locked, setLocked] = useState(false); useEffect(() => { if (item.type === "note") { @@ -99,46 +170,41 @@ export const useActions = ({ } }, [item]); - const checkNotifPinned = useCallback(() => { - const pinned = Notifications.getPinnedNotes(); - - if (!pinned || pinned.length === 0) { - setNotifPinned(undefined); - return; - } - const index = pinned.findIndex((notif) => notif.id === item.id); - if (index !== -1) { - setNotifPinned(pinned[index]); - } else { - setNotifPinned(undefined); - } - }, [item.id]); - useEffect(() => { if (item.type !== "note") return; - checkNotifPinned(); + setNotifPinned(isNotePinnedInNotifications(item)); setIsPinnedToMenu(db.shortcuts.exists(item.id)); - }, [checkNotifPinned, item]); - - const onUpdate = useCallback( - async (type: string) => { - if (type === "unpin") { - await Notifications.get(); - checkNotifPinned(); - } - }, - [checkNotifPinned] - ); + }, [item]); useEffect(() => { - const sub = eSubscribeEvent(Notifications.Events.onUpdate, onUpdate); + const { currentRoute, focusedRouteId } = useNavigationStore.getState(); + if (item.type !== "note" || currentRoute !== "Notebook" || !focusedRouteId) + return; + + !!db.relations + .to(item, "notebook") + .selector.find((v) => v("id", "==", focusedRouteId)) + .then((notebook) => { + setNoteInCurrentNotebook(!!notebook); + }); + }, [item]); + + useEffect(() => { + const sub = eSubscribeEvent( + Notifications.Events.onUpdate, + async (type: string) => { + if (type === "unpin") { + await Notifications.get(); + setNotifPinned(isNotePinnedInNotifications(item)); + } + } + ); return () => { sub?.unsubscribe(); }; - }, [item, onUpdate]); + }, [item]); async function restoreTrashItem() { - if (!checkItemSynced()) return; close(); if ((await db.trash.restore(item.id)) === false) return; Navigation.queueRoutesForUpdate(); @@ -154,20 +220,16 @@ export const useActions = ({ async function pinItem() { if (!item.id) return; - close(); if (item.type === "note") { await db.notes.pin(!item?.pinned, item.id); } else if (item.type === "notebook") { await db.notebooks.pin(!item?.pinned, item.id); } + close(); Navigation.queueRoutesForUpdate(); } - const checkItemSynced = () => { - return true; - }; - async function createMenuShortcut() { if (item.type !== "notebook" && item.type !== "tag") return; @@ -299,7 +361,6 @@ export const useActions = ({ async function deleteTrashItem() { if (item.type !== "trash") return; - if (!checkItemSynced()) return; close(); await sleep(300); presentDialog({ @@ -323,42 +384,14 @@ export const useActions = ({ }); } - const actions: { - id: string; - title: string; - icon: string; - func: () => void; - close?: boolean; - check?: boolean; - on?: boolean; - pro?: boolean; - switch?: boolean; - hidden?: boolean; - type?: string; - color?: string; - }[] = [ - // { - // id: "ReferencedIn", - // title: "References", - // icon: "link", - // func: async () => { - // close(); - // RelationsList.present({ - // reference: item, - // referenceType: "note", - // title: "Referenced in", - // relationType: "to", - // }); - // } - // } - ]; + const actions: Action[] = []; if (item.type === "tag") { actions.push({ id: "rename-tag", title: strings.rename(), icon: "square-edit-outline", - func: renameTag + onPress: renameTag }); } @@ -367,14 +400,14 @@ export const useActions = ({ id: "rename-color", title: strings.rename(), icon: "square-edit-outline", - func: renameColor + onPress: renameColor }); actions.push({ id: "reorder", title: strings.reorder(), icon: "sort-ascending", - func: () => { + onPress: () => { useSideBarDraggingStore.setState({ dragging: true }); @@ -391,7 +424,7 @@ export const useActions = ({ ? strings.turnOffReminder() : strings.turnOnReminder(), icon: !item.disabled ? "bell-off-outline" : "bell", - func: async () => { + onPress: async () => { close(); await db.reminders.add({ ...item, @@ -406,10 +439,9 @@ export const useActions = ({ id: "edit-reminder", title: strings.editReminder(), icon: "pencil", - func: async () => { + onPress: async () => { ReminderSheet.present(item); - }, - close: false + } } ); } @@ -420,13 +452,13 @@ export const useActions = ({ id: "restore", title: strings.restore(), icon: "delete-restore", - func: restoreTrashItem + onPress: restoreTrashItem }, { id: "delete", title: strings.delete(), icon: "delete", - func: deleteTrashItem + onPress: deleteTrashItem } ); } @@ -436,11 +468,27 @@ export const useActions = ({ id: "add-shortcut", title: isPinnedToMenu ? strings.removeShortcut() : strings.addShortcut(), icon: isPinnedToMenu ? "link-variant-remove" : "link-variant", - func: createMenuShortcut, - close: false, - check: true, - on: isPinnedToMenu, - pro: true + onPress: createMenuShortcut, + isToggle: true, + checked: isPinnedToMenu, + activeColor: colors.error.paragraph + }); + actions.push({ + id: "select", + title: strings.select() + " " + strings.dataTypes[item.type](), + icon: "checkbox-outline", + onPress: () => { + const store = + item.type === "tag" + ? useSideMenuTagsSelectionStore + : useSideMenuNotebookSelectionStore; + store.setState({ + enabled: true, + selection: {} + }); + store.getState().markAs(item, "selected"); + close(); + } }); } @@ -450,7 +498,7 @@ export const useActions = ({ id: "add-notebook", title: strings.addNotebook(), icon: "plus", - func: async () => { + onPress: async () => { AddNotebookSheet.present(undefined, item); } }, @@ -458,7 +506,7 @@ export const useActions = ({ id: "edit-notebook", title: strings.editNotebook(), icon: "square-edit-outline", - func: async () => { + onPress: async () => { AddNotebookSheet.present(item); } }, @@ -470,7 +518,7 @@ export const useActions = ({ : strings.setAsDefault(), hidden: item.type !== "notebook", icon: "notebook", - func: async () => { + onPress: async () => { if (defaultNotebook === item.id) { await db.settings.setDefaultNotebook(undefined); setDefaultNotebook(undefined); @@ -483,14 +531,14 @@ export const useActions = ({ } close(); }, - on: defaultNotebook === item.id + checked: defaultNotebook === item.id }, { id: "move-notes", title: strings.moveNotes(), hidden: item.type !== "notebook", icon: "text", - func: () => { + onPress: () => { MoveNotes.present(item); } }, @@ -498,7 +546,7 @@ export const useActions = ({ id: "move-notebook", title: strings.moveNotebookFix(), icon: "arrow-right-bold-box-outline", - func: () => { + onPress: () => { MoveNotebookSheet.present([item]); } } @@ -510,10 +558,9 @@ export const useActions = ({ id: "pin", title: item.pinned ? strings.unpin() : strings.pin(), icon: item.pinned ? "pin-off-outline" : "pin-outline", - func: pinItem, - close: false, - check: true, - on: item.pinned, + onPress: pinItem, + isToggle: true, + checked: item.pinned, pro: true }); } @@ -535,7 +582,7 @@ export const useActions = ({ } async function toggleLocalOnly() { - if (!checkItemSynced() || !user) return; + if (!user) return; await db.notes.localOnly(!(item as Note).localOnly, item?.id); Navigation.queueRoutesForUpdate(); close(); @@ -556,7 +603,6 @@ export const useActions = ({ }; const duplicateNote = async () => { - if (!checkItemSynced()) return; await db.notes.duplicate(item.id); Navigation.queueRoutesForUpdate(); close(); @@ -577,19 +623,16 @@ export const useActions = ({ async function addToFavorites() { if (!item.id || item.type !== "note") return; - close(); await db.notes.favorite(!item.favorite, item.id); Navigation.queueRoutesForUpdate(); + close(); } async function pinToNotifications() { - if (!checkItemSynced()) return; - if (Platform.OS === "ios") return; - if (notifPinned) { Notifications.remove(item.id); await Notifications.get(); - checkNotifPinned(); + setNotifPinned(isNotePinnedInNotifications(item)); return; } if (locked) { @@ -603,7 +646,7 @@ export const useActions = ({ const text = await convertNoteToText(item as Note, true); const html = (text || "").replace(/\n/g, "<br />"); await Notifications.displayNotification({ - title: item.title, + title: (item as Note).title, message: (item as Note).headline || text || "", subtitle: "", bigText: html, @@ -612,11 +655,10 @@ export const useActions = ({ id: item.id }); await Notifications.get(); - checkNotifPinned(); + setNotifPinned(isNotePinnedInNotifications(item)); } async function publishNote() { - if (!checkItemSynced()) return; if (!user) { ToastManager.show({ heading: strings.loginRequired(), @@ -657,7 +699,6 @@ export const useActions = ({ }); return; } - if (!checkItemSynced()) return; if (locked) { close(); await sleep(300); @@ -687,8 +728,6 @@ export const useActions = ({ async function addToVault() { if (item.type !== "note") return; - - if (!checkItemSynced()) return; if (locked) { close(); await sleep(300); @@ -709,10 +748,10 @@ export const useActions = ({ Navigation.queueRoutesForUpdate(); eSendEvent(eUpdateNoteInEditor, item, true); } - } catch (e: any) { + } catch (e: unknown) { close(); await sleep(300); - switch (e.message) { + switch ((e as Error).message) { case VAULT_ERRORS.noVault: openVault({ item: item, @@ -775,37 +814,36 @@ export const useActions = ({ id: "favorite", title: !item.favorite ? strings.favorite() : strings.unfavorite(), icon: item.favorite ? "star-off" : "star-outline", - func: addToFavorites, - close: false, - check: true, - on: item.favorite, + onPress: addToFavorites, + isToggle: true, + checked: item.favorite, pro: true, - color: "orange" + activeColor: "orange" }, { id: "remove-from-notebook", title: strings.removeFromNotebook(), hidden: noteInCurrentNotebook, icon: "minus-circle-outline", - func: removeNoteFromNotebook + onPress: removeNoteFromNotebook }, { id: "attachments", title: strings.attachments(), icon: "attachment", - func: showAttachments + onPress: showAttachments }, { id: "history", title: strings.history(), icon: "history", - func: openHistory + onPress: openHistory }, { id: "copy-link", title: strings.copyLink(), icon: "link", - func: () => { + onPress: () => { Clipboard.setString(createInternalLink("note", item.id)); ToastManager.show({ heading: strings.linkCopied(), @@ -819,7 +857,7 @@ export const useActions = ({ id: "reminders", title: strings.dataTypesPluralCamelCase.reminder(), icon: "clock-outline", - func: async () => { + onPress: async () => { RelationsList.present({ reference: item, referenceType: "reminder", @@ -833,121 +871,108 @@ export const useActions = ({ icon: "plus" } }); - }, - close: false + } }, { id: "copy", title: strings.copy(), icon: "content-copy", - func: copyContent + onPress: copyContent }, { id: "share", title: strings.share(), icon: "share-variant", - func: shareNote + onPress: shareNote }, { id: "read-only", title: strings.readOnly(), icon: "pencil-lock", - func: toggleReadyOnlyMode, - on: item.readonly + onPress: toggleReadyOnlyMode, + checked: item.readonly }, { id: "local-only", title: strings.syncOff(), icon: "sync-off", - func: toggleLocalOnly, - on: item.localOnly + onPress: toggleLocalOnly, + checked: item.localOnly }, { id: "duplicate", title: strings.duplicate(), icon: "content-duplicate", - func: duplicateNote + onPress: duplicateNote }, { id: "add-reminder", title: strings.remindMe(), icon: "clock-plus-outline", - func: () => { + onPress: () => { ReminderSheet.present(undefined, { id: item.id, type: "note" }); - }, - close: true + } }, { id: "lock-unlock", title: locked ? strings.unlock() : strings.lock(), icon: locked ? "lock-open-outline" : "key-outline", - func: addToVault, - on: locked + onPress: addToVault, + checked: locked }, { id: "publish", title: isPublished ? strings.published() : strings.publish(), icon: "cloud-upload-outline", - on: isPublished, - func: publishNote + checked: isPublished, + onPress: publishNote }, { id: "export", title: strings.export(), icon: "export", - func: exportNote - }, - - { - id: "pin-to-notifications", - title: notifPinned - ? strings.unpinFromNotifications() - : strings.pinToNotifications(), - icon: "message-badge-outline", - on: !!notifPinned, - func: pinToNotifications + onPress: exportNote }, { id: "notebooks", title: strings.linkNotebooks(), icon: "book-outline", - func: addTo + onPress: addTo }, { id: "add-tag", title: strings.addTags(), icon: "pound", - func: addTo + onPress: addTo }, { id: "references", title: strings.references(), icon: "vector-link", - func: () => { + onPress: () => { ReferencesList.present({ reference: item as ItemReference }); } } ); - } - useEffect(() => { - const { currentRoute, focusedRouteId } = useNavigationStore.getState(); - if (item.type !== "note" || currentRoute !== "Notebook" || !focusedRouteId) - return; - - !!db.relations - .to(item, "notebook") - .selector.find((v) => v("id", "==", focusedRouteId)) - .then((notebook) => { - setNoteInCurrentNotebook(!!notebook); + if (Platform.OS === "android") { + actions.push({ + id: "pin-to-notifications", + title: notifPinned + ? strings.unpinFromNotifications() + : strings.pinToNotifications(), + icon: "message-badge-outline", + checked: !!notifPinned, + onPress: pinToNotifications }); - }, [item]); + } + } actions.push({ id: "trash", @@ -960,7 +985,7 @@ export const useActions = ({ : strings.moveToTrash(), icon: "delete-outline", type: "error", - func: deleteItem + onPress: deleteItem }); return actions; diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index b005e9dd6..1278b24bd 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -59,8 +59,7 @@ import { import { clearAppState, editorController, - editorState, - setAppState + editorState } from "../screens/editor/tiptap/utils"; import { useDragState } from "../screens/settings/editor/state"; import BackupService from "../services/backup"; @@ -103,7 +102,7 @@ import { refreshNotesPage } from "../utils/events"; import { getGithubVersion } from "../utils/github-version"; -import { tabBarRef } from "../utils/global-refs"; +import { fluidTabsRef } from "../utils/global-refs"; import { NotesnookModule } from "../utils/notesnook-module"; import { sleep } from "../utils/time"; @@ -168,7 +167,7 @@ const onAppOpenedFromURL = async (event: { url: string }) => { clearAppState(); editorState().movedAway = false; eSendEvent(eOnLoadNote, { newNote: true }); - tabBarRef.current?.goToPage(1, false); + fluidTabsRef.current?.goToPage(1, false); return; } else if (url.startsWith("https://notesnook.com/open_note")) { const id = new URL(url).searchParams.get("id"); @@ -178,7 +177,7 @@ const onAppOpenedFromURL = async (event: { url: string }) => { eSendEvent(eOnLoadNote, { item: note }); - tabBarRef.current?.goToPage(1, false); + fluidTabsRef.current?.goToPage(1, false); } } } else if (url.startsWith("https://notesnook.com/open_reminder")) { diff --git a/apps/mobile/app/hooks/use-db-item.ts b/apps/mobile/app/hooks/use-db-item.ts index df52a9f10..74ebc1e66 100644 --- a/apps/mobile/app/hooks/use-db-item.ts +++ b/apps/mobile/app/hooks/use-db-item.ts @@ -19,13 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import { Attachment, Color, + HistorySession, Note, Notebook, Reminder, Shortcut, Tag, - VirtualizedGrouping, - HistorySession + VirtualizedGrouping } from "@notesnook/core"; import React, { useEffect, useRef, useState } from "react"; import { db } from "../common/database"; @@ -34,8 +34,8 @@ import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager"; -import { eDBItemUpdate } from "../utils/events"; import { useSettingStore } from "../stores/use-setting-store"; +import { eDBItemUpdate } from "../utils/events"; type ItemTypeKey = { note: Note; @@ -55,7 +55,8 @@ function isValidIdOrIndex(idOrIndex?: string | number) { export const useDBItem = <T extends keyof ItemTypeKey>( idOrIndex?: string | number, type?: T, - items?: VirtualizedGrouping<ItemTypeKey[T]> + items?: VirtualizedGrouping<ItemTypeKey[T]>, + onItemUpdated?: (item?: ItemTypeKey[T]) => void ): [ItemTypeKey[T] | undefined, () => void] => { const [item, setItem] = useState<ItemTypeKey[T]>(); const itemIdRef = useRef<string>(); @@ -67,15 +68,15 @@ export const useDBItem = <T extends keyof ItemTypeKey>( } useEffect(() => { - const onUpdateItem = (itemId?: string) => { + const onUpdateItem = async (itemId?: string) => { if (typeof itemId === "string" && itemId !== itemIdRef.current) return; if (!isValidIdOrIndex(idOrIndex)) return; if (items && typeof idOrIndex === "number") { - items.item(idOrIndex).then((item) => { - setItem(item.item); - itemIdRef.current = item.item?.id; - }); + const item = (await items.item(idOrIndex))?.item; + setItem(item); + itemIdRef.current = item?.id; + onItemUpdated?.(item); } else { if (!(db as any)[type + "s"][type]) { console.warn( @@ -83,12 +84,12 @@ export const useDBItem = <T extends keyof ItemTypeKey>( `db.${type}s.${type}(id: string)` ); } else { - (db as any)[type + "s"] - ?.[type]?.(idOrIndex as string) - .then((item: ItemTypeKey[T]) => { - setItem(item); - itemIdRef.current = item.id; - }); + const item = await (db as any)[type + "s"]?.[type]?.( + idOrIndex as string + ); + setItem(item); + itemIdRef.current = item.id; + onItemUpdated?.(item); } } }; @@ -110,7 +111,7 @@ export const useDBItem = <T extends keyof ItemTypeKey>( return () => { eUnSubscribeEvent(eDBItemUpdate, onUpdateItem); }; - }, [idOrIndex, type, items]); + }, [idOrIndex, type, items, onItemUpdated]); return [ isValidIdOrIndex(idOrIndex) ? (item as ItemTypeKey[T]) : undefined, diff --git a/apps/mobile/app/hooks/use-notebook.ts b/apps/mobile/app/hooks/use-notebook.ts index 7303e5006..8096033a9 100644 --- a/apps/mobile/app/hooks/use-notebook.ts +++ b/apps/mobile/app/hooks/use-notebook.ts @@ -17,7 +17,7 @@ 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 React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { db } from "../common/database"; import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager"; import { eGroupOptionsUpdated, eOnNotebookUpdated } from "../utils/events"; @@ -26,56 +26,61 @@ import { useDBItem, useTotalNotes } from "./use-db-item"; export const useNotebook = ( id?: string | number, items?: VirtualizedGrouping<Notebook>, - nestedNotebooks?: boolean + nestedNotebooks?: boolean, + countNotes?: boolean ) => { - const [item, refresh] = useDBItem(id, "notebook", items); const groupOptions = db.settings.getGroupOptions("notebooks"); const [notebooks, setNotebooks] = useState<VirtualizedGrouping<Notebook>>(); const { totalNotes: nestedNotebookNotesCount, getTotalNotes } = useTotalNotes("notebook"); + const getTotalNotesRef = useRef(getTotalNotes); + getTotalNotesRef.current = getTotalNotes; + const onItemUpdated = React.useCallback( + (item?: Notebook) => { + if (!item) return; - const onRequestUpdate = React.useCallback(() => { - if (!item?.id) return; + if (nestedNotebooks) { + const selector = db.relations.from( + { + type: "notebook", + id: item.id + }, + "notebook" + ).selector; + selector.ids().then((notebookIds) => { + getTotalNotesRef.current(notebookIds); + }); - const selector = db.relations.from( - { - type: "notebook", - id: item.id - }, - "notebook" - ).selector; + selector + .sorted(db.settings.getGroupOptions("notebooks")) + .then((notebooks) => { + setNotebooks(notebooks); + }); + } - selector.ids().then((notebookIds) => { - getTotalNotes(notebookIds); - }); + if (countNotes) { + getTotalNotesRef.current([item?.id]); + } + }, + [countNotes, nestedNotebooks] + ); - selector - .sorted(db.settings.getGroupOptions("notebooks")) - .then((notebooks) => { - setNotebooks(notebooks); - }); - }, [getTotalNotes, item?.id]); + const [item, refresh] = useDBItem(id, "notebook", items, onItemUpdated); - useEffect(() => { - if (nestedNotebooks) { - onRequestUpdate(); - } - }, [item?.id, onRequestUpdate, nestedNotebooks]); + const itemRef = useRef(item); + itemRef.current = item; + const refreshRef = useRef(refresh); + refreshRef.current = refresh; useEffect(() => { const onNotebookUpdate = (id?: string) => { if (typeof id === "string" && id !== id) return; - setImmediate(() => { - if (nestedNotebooks) { - onRequestUpdate(); - } - refresh(); - }); + refreshRef.current(); }; const onUpdate = (type: string) => { if (type !== "notebooks") return; - onRequestUpdate(); + refreshRef.current(); }; eSubscribeEvent(eGroupOptionsUpdated, onUpdate); @@ -84,13 +89,14 @@ export const useNotebook = ( eUnSubscribeEvent(eGroupOptionsUpdated, onUpdate); eUnSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate); }; - }, [onRequestUpdate, item?.id, refresh, nestedNotebooks]); + }, [nestedNotebooks]); return { notebook: item, nestedNotebookNotesCount, nestedNotebooks: item ? notebooks : undefined, - onUpdate: onRequestUpdate, - groupOptions + onUpdate: () => refresh(), + groupOptions, + notesCount: !item ? 0 : nestedNotebookNotesCount(item?.id) }; }; diff --git a/apps/mobile/app/hooks/use-shortcut-manager.ts b/apps/mobile/app/hooks/use-shortcut-manager.ts index 16f9aae68..e72255a03 100644 --- a/apps/mobile/app/hooks/use-shortcut-manager.ts +++ b/apps/mobile/app/hooks/use-shortcut-manager.ts @@ -44,7 +44,7 @@ export const useShortcutManager = ({ shortcuts = defaultShortcuts }: { onShortcutPressed: (shortcut: ShortcutItem | null) => void; - shortcuts: ShortcutItem[]; + shortcuts?: ShortcutItem[]; }) => { const initialShortcutRecieved = useRef(false); diff --git a/apps/mobile/app/navigation/fluid-panels-view.tsx b/apps/mobile/app/navigation/fluid-panels-view.tsx new file mode 100644 index 000000000..5059ebb02 --- /dev/null +++ b/apps/mobile/app/navigation/fluid-panels-view.tsx @@ -0,0 +1,559 @@ +/* +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 { ScopedThemeProvider, useThemeColors } from "@notesnook/theme"; +import { + activateKeepAwake, + deactivateKeepAwake +} from "@sayem314/react-native-keep-awake"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState +} from "react"; +import { Dimensions, LayoutChangeEvent, Platform, View } from "react-native"; +import { + addOrientationListener, + addSpecificOrientationListener, + getInitialOrientation, + getSpecificOrientation, + removeOrientationListener, + removeSpecificOrientationListener +} from "react-native-orientation"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming +} from "react-native-reanimated"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { notesnook } from "../../e2e/test.ids"; +import { db } from "../common/database"; +import { FluidPanels } from "../components/fluid-panels"; +import { SideMenu } from "../components/side-menu"; +import { useSideBarDraggingStore } from "../components/side-menu/dragging-store"; +import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets"; +import { useShortcutManager } from "../hooks/use-shortcut-manager"; +import { hideAllTooltips } from "../hooks/use-tooltip"; +import { useTabStore } from "../screens/editor/tiptap/use-tab-store"; +import { + clearAppState, + editorController, + editorState, + getAppState +} from "../screens/editor/tiptap/utils"; +import { EditorWrapper } from "../screens/editor/wrapper"; +import { DDS } from "../services/device-detection"; +import { + eSendEvent, + eSubscribeEvent, + eUnSubscribeEvent +} from "../services/event-manager"; +import { useSettingStore } from "../stores/use-setting-store"; +import { + eCloseFullscreenEditor, + eOnEnterEditor, + eOnExitEditor, + eOnLoadNote, + eOpenFullscreenEditor, + eUnlockNote +} from "../utils/events"; +import { editorRef, fluidTabsRef } from "../utils/global-refs"; +import { AppNavigationStack } from "./navigation-stack"; + +const MOBILE_SIDEBAR_SIZE = 0.85; + +const valueLimiter = (value: number, min: number, max: number) => { + return value < min ? min : value > max ? max : value; +}; + +export const FluidPanelsView = React.memo( + () => { + const { colors } = useThemeColors(); + const deviceMode = useSettingStore((state) => state.deviceMode); + const setFullscreen = useSettingStore((state) => state.setFullscreen); + const fullscreen = useSettingStore((state) => state.fullscreen); + const setDeviceModeState = useSettingStore((state) => state.setDeviceMode); + const dimensions = useSettingStore((state) => state.dimensions); + const setDimensions = useSettingStore((state) => state.setDimensions); + const insets = useGlobalSafeAreaInsets(); + const animatedOpacity = useSharedValue(0); + const animatedTranslateY = useSharedValue(-9999); + const overlayRef = useRef<Animated.View>(null); + const [orientation, setOrientation] = useState(getInitialOrientation()); + + useShortcutManager({ + onShortcutPressed: async (item) => { + if (!item && getAppState()) { + editorState().movedAway = false; + fluidTabsRef.current?.goToPage(1, false); + return; + } + if (item?.type === "notesnook.action.newnote") { + clearAppState(); + if (!fluidTabsRef.current) { + setTimeout(() => { + eSendEvent(eOnLoadNote, { newNote: true }); + editorState().movedAway = false; + fluidTabsRef.current?.goToPage(1, false); + }, 3000); + return; + } + eSendEvent(eOnLoadNote, { newNote: true }); + editorState().movedAway = false; + fluidTabsRef.current?.goToPage(1, false); + } + } + }); + + useEffect(() => { + const onOrientationChange = (o: string, o2: string) => { + setOrientation(o || o2); + }; + + if (Platform.OS === "ios") { + addSpecificOrientationListener(onOrientationChange); + getSpecificOrientation && getSpecificOrientation(onOrientationChange); + } else { + addOrientationListener(onOrientationChange); + } + + return () => { + removeSpecificOrientationListener(onOrientationChange); + removeOrientationListener(onOrientationChange); + }; + }, []); + + const showFullScreenEditor = useCallback(() => { + setFullscreen(true); + if (deviceMode === "smallTablet") { + fluidTabsRef.current?.openDrawer(false); + } + editorRef.current?.setNativeProps({ + style: { + width: dimensions.width, + zIndex: 999, + paddingHorizontal: + deviceMode === "smallTablet" + ? dimensions.width * 0 + : dimensions.width * 0.15 + } + }); + }, [deviceMode, dimensions.width, setFullscreen]); + + const closeFullScreenEditor = useCallback( + (current: string) => { + const _deviceMode = current || deviceMode; + if (_deviceMode === "smallTablet") { + fluidTabsRef.current?.closeDrawer(false); + } + setFullscreen(false); + editorController.current?.commands.updateSettings({ + fullscreen: false + }); + editorRef.current?.setNativeProps({ + style: { + width: + _deviceMode === "smallTablet" + ? dimensions.width - + valueLimiter(dimensions.width * 0.4, 300, 450) + : dimensions.width * 0.48, + zIndex: null, + paddingHorizontal: 0 + } + }); + if (_deviceMode === "smallTablet") { + fluidTabsRef.current?.goToIndex(1, false); + } + if (_deviceMode === "mobile") { + fluidTabsRef.current?.goToIndex(2, false); + } + }, + [deviceMode, dimensions.width, setFullscreen] + ); + + const toggleView = useCallback( + (show: boolean) => { + animatedTranslateY.value = show ? 0 : -9999; + }, + [animatedTranslateY] + ); + + useEffect(() => { + if (!fluidTabsRef.current?.isDrawerOpen()) { + toggleView(false); + } + eSubscribeEvent(eOpenFullscreenEditor, showFullScreenEditor); + eSubscribeEvent(eCloseFullscreenEditor, closeFullScreenEditor); + + return () => { + eUnSubscribeEvent(eOpenFullscreenEditor, showFullScreenEditor); + eUnSubscribeEvent(eCloseFullscreenEditor, closeFullScreenEditor); + }; + }, [ + deviceMode, + dimensions, + colors, + showFullScreenEditor, + closeFullScreenEditor, + toggleView + ]); + + const setDeviceMode = React.useCallback( + (current: string | null, size: { width: number; height: number }) => { + setDeviceModeState(current); + + const needsLayout = current !== deviceMode; + + if (fullscreen && current !== "mobile") { + // Runs after size is set via state. + setTimeout(() => { + editorRef.current?.setNativeProps({ + style: { + width: size.width, + zIndex: 999, + paddingHorizontal: + current === "smallTablet" ? size.width * 0 : size.width * 0.15 + } + }); + }, 1); + } else { + if (fullscreen) eSendEvent(eCloseFullscreenEditor, current); + editorRef.current?.setNativeProps({ + style: { + position: "relative", + width: + current === "tablet" + ? size.width * 0.48 + : current === "smallTablet" + ? size.width - valueLimiter(size.width * 0.4, 300, 450) + : size.width, + zIndex: null, + paddingHorizontal: 0 + } + }); + } + if (!needsLayout) { + return; + } + + const state = getAppState(); + switch (current) { + case "tablet": + fluidTabsRef.current?.goToIndex(0, false); + break; + case "smallTablet": + if (!fullscreen) { + fluidTabsRef.current?.closeDrawer(false); + } else { + fluidTabsRef.current?.openDrawer(false); + } + break; + case "mobile": + if ( + state && + editorState().movedAway === false && + useTabStore.getState().getCurrentNoteId() + ) { + fluidTabsRef.current?.goToIndex(2, false); + } else { + fluidTabsRef.current?.goToIndex(1, false); + } + break; + } + }, + [deviceMode, fullscreen, setDeviceModeState] + ); + + const checkDeviceType = React.useCallback( + (size: { width: number; height: number }) => { + setDimensions({ + width: size.width, + height: size.height + }); + DDS.setSize(size, orientation); + const nextDeviceMode = DDS.isLargeTablet() + ? "tablet" + : DDS.isSmallTab + ? "smallTablet" + : "mobile"; + + setDeviceMode(nextDeviceMode, size); + }, + [orientation, setDeviceMode, setDimensions] + ); + + const _onLayout = React.useCallback( + (event: LayoutChangeEvent) => { + const size = event?.nativeEvent?.layout; + if (!size || (size.width === dimensions.width && deviceMode !== null)) { + DDS.setSize(size, orientation); + setDeviceMode(deviceMode, size); + checkDeviceType(size); + return; + } + + checkDeviceType(size); + }, + [ + checkDeviceType, + deviceMode, + dimensions.width, + orientation, + setDeviceMode + ] + ); + + if (!deviceMode) { + const size = Dimensions.get("window"); + checkDeviceType(size); + } + + const PANE_OFFSET = useMemo( + () => ({ + mobile: { + sidebar: dimensions.width * MOBILE_SIDEBAR_SIZE, + list: dimensions.width + dimensions.width * MOBILE_SIDEBAR_SIZE, + editor: dimensions.width * 2 + dimensions.width * MOBILE_SIDEBAR_SIZE + }, + smallTablet: { + sidebar: fullscreen + ? 0 + : valueLimiter(dimensions.width * 0.3, 300, 350), + list: fullscreen + ? 0 + : dimensions.width + valueLimiter(dimensions.width * 0.3, 300, 350), + editor: fullscreen + ? 0 + : dimensions.width + valueLimiter(dimensions.width * 0.3, 300, 350) + }, + tablet: { + sidebar: 0, + list: 0, + editor: 0 + } + }), + [dimensions.width, fullscreen] + ); + + const PANE_WIDTHS = useMemo( + () => ({ + mobile: { + sidebar: dimensions.width * MOBILE_SIDEBAR_SIZE, + list: dimensions.width, + editor: dimensions.width + }, + smallTablet: { + sidebar: valueLimiter(dimensions.width * 0.3, 300, 350), + list: valueLimiter(dimensions.width * 0.4, 300, 450), + editor: + dimensions.width - valueLimiter(dimensions.width * 0.4, 300, 450) + }, + tablet: { + sidebar: dimensions.width * 0.22, + list: dimensions.width * 0.3, + editor: dimensions.width * 0.48 + } + }), + [dimensions.width] + ); + + const onScroll = React.useCallback( + (scrollOffset: number) => { + if (!deviceMode) return; + hideAllTooltips(); + if ( + scrollOffset > + PANE_OFFSET[deviceMode as keyof typeof PANE_OFFSET].sidebar - 10 + ) { + animatedOpacity.value = 0; + toggleView(false); + } else { + const o = scrollOffset / 300; + const opacity = o < 0 ? 1 : 1 - o; + animatedOpacity.value = opacity; + toggleView(opacity < 0.1 ? false : true); + } + }, + [PANE_OFFSET, animatedOpacity, deviceMode, toggleView] + ); + + const animatedStyle = useAnimatedStyle(() => { + return { + opacity: animatedOpacity.value, + transform: [ + { + translateY: animatedTranslateY.value + } + ] + }; + }, []); + + return ( + <View + onLayout={_onLayout} + testID={notesnook.ids.default.root} + style={{ + height: "100%", + width: "100%", + backgroundColor: colors.primary.background, + paddingBottom: Platform.OS === "android" ? insets?.bottom : 0, + marginRight: + orientation === "LANDSCAPE-RIGHT" && Platform.OS === "ios" + ? insets.right + : 0, + marginLeft: + orientation === "LANDSCAPE-LEFT" && Platform.OS === "ios" + ? insets.left + : 0 + }} + > + {deviceMode && PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS] ? ( + <FluidPanels + ref={fluidTabsRef} + dimensions={dimensions} + widths={PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS]} + enabled={deviceMode !== "tablet" && !fullscreen} + onScroll={onScroll} + onChangeTab={onChangeTab} + onDrawerStateChange={(state) => { + if (!state) { + useSideBarDraggingStore.setState({ + dragging: false + }); + } + }} + > + <View + key="1" + style={{ + height: "100%", + width: fullscreen + ? 0 + : PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS]?.sidebar + }} + > + <ScopedThemeProvider value="navigationMenu"> + <SideMenu /> + </ScopedThemeProvider> + </View> + + <View + key="2" + style={{ + height: "100%", + width: fullscreen + ? 0 + : PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS]?.list + }} + > + <ScopedThemeProvider value="list"> + {deviceMode === "mobile" ? ( + <Animated.View + onTouchEnd={() => { + if (useSideBarDraggingStore.getState().dragging) { + useSideBarDraggingStore.setState({ + dragging: false + }); + return; + } + fluidTabsRef.current?.closeDrawer(); + animatedOpacity.value = withTiming(0); + animatedTranslateY.value = withTiming(-9999); + }} + style={[ + { + position: "absolute", + width: "100%", + height: "100%", + zIndex: 999, + backgroundColor: colors.primary.backdrop + }, + animatedStyle + ]} + ref={overlayRef} + /> + ) : null} + + <SafeAreaView + style={{ + flex: 1 + }} + > + <AppNavigationStack /> + </SafeAreaView> + </ScopedThemeProvider> + </View> + + <ScopedThemeProvider value="editor"> + <EditorWrapper key="3" widths={PANE_WIDTHS} /> + </ScopedThemeProvider> + </FluidPanels> + ) : null} + </View> + ); + }, + () => true +); +FluidPanelsView.displayName = "FluidPanelsView"; + +const onChangeTab = async (event: { i: number; from: number }) => { + if (event.i === 2) { + editorState().movedAway = false; + editorState().isFocused = true; + activateKeepAwake(); + eSendEvent(eOnEnterEditor); + + if (!useTabStore.getState().getCurrentNoteId()) { + eSendEvent(eOnLoadNote, { + newNote: true + }); + } else { + if ( + useTabStore.getState().getTab(useTabStore.getState().currentTab) + ?.session?.locked + ) { + eSendEvent(eUnlockNote); + } + } + } else { + if (event.from === 2) { + deactivateKeepAwake(); + editorState().movedAway = true; + editorState().isFocused = false; + eSendEvent(eOnExitEditor); + + // Lock all tabs with locked notes... + for (const tab of useTabStore.getState().tabs) { + const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId; + if (!noteId) continue; + const note = await db.notes.note(noteId); + const locked = note && (await db.vaults.itemExists(note)); + if (locked) { + useTabStore.getState().updateTab(tab.id, { + session: { + locked: true + } + }); + } + } + } + } +}; diff --git a/apps/mobile/app/navigation/navigation-stack.js b/apps/mobile/app/navigation/navigation-stack.js deleted file mode 100644 index 187b82a1c..000000000 --- a/apps/mobile/app/navigation/navigation-stack.js +++ /dev/null @@ -1,150 +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 { strings } from "@notesnook/intl"; -import { useThemeColors } from "@notesnook/theme"; -import { NavigationContainer } from "@react-navigation/native"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import * as React from "react"; -import Container from "../components/container"; -import Intro from "../components/intro"; -import { NotebookSheet } from "../components/sheets/notebook-sheet"; -import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets"; -import { hideAllTooltips } from "../hooks/use-tooltip"; -import Favorites from "../screens/favorites"; -import Home from "../screens/home"; -import NotebookScreen from "../screens/notebook"; -import Notebooks from "../screens/notebooks"; -import { ColoredNotes } from "../screens/notes/colored"; -import { Monographs } from "../screens/notes/monographs"; -import { TaggedNotes } from "../screens/notes/tagged"; -import Reminders from "../screens/reminders"; -import { Search } from "../screens/search"; -import Settings from "../screens/settings"; -import Tags from "../screens/tags"; -import Trash from "../screens/trash"; -import { eSendEvent } from "../services/event-manager"; -import SettingsService from "../services/settings"; -import useNavigationStore from "../stores/use-navigation-store"; -import { useSelectionStore } from "../stores/use-selection-store"; -import { useSettingStore } from "../stores/use-setting-store"; -import { rootNavigatorRef } from "../utils/global-refs"; - -const NativeStack = createNativeStackNavigator(); -const IntroStack = createNativeStackNavigator(); - -const IntroStackNavigator = () => { - const { colors } = useThemeColors(); - const height = useSettingStore((state) => state.dimensions.height); - return ( - <IntroStack.Navigator - screenOptions={{ - headerShown: false, - lazy: false, - animation: "none", - contentStyle: { - backgroundColor: colors.primary.background, - minHeight: height - } - }} - initialRouteName={"Intro"} - > - <NativeStack.Screen name="Intro" component={Intro} /> - </IntroStack.Navigator> - ); -}; - -const _Tabs = () => { - const { colors } = useThemeColors(); - const homepage = SettingsService.get().homepage; - const introCompleted = useSettingStore( - (state) => state.settings.introCompleted - ); - - const height = useSettingStore((state) => state.dimensions.height); - const insets = useGlobalSafeAreaInsets(); - const screenHeight = height - (50 + insets.top + insets.bottom); - React.useEffect(() => { - setTimeout(async () => { - useNavigationStore.getState().update(homepage); - }, 1000); - }, [homepage]); - - return ( - <NativeStack.Navigator - tabBar={() => null} - initialRouteName={!introCompleted ? "Welcome" : homepage} - backBehavior="history" - screenOptions={{ - headerShown: false, - lazy: false, - animation: "none", - contentStyle: { - backgroundColor: colors.primary.background, - minHeight: !introCompleted ? undefined : screenHeight - } - }} - > - <NativeStack.Screen name="Welcome" component={IntroStackNavigator} /> - <NativeStack.Screen name="Notes" component={Home} /> - <NativeStack.Screen name="Notebooks" component={Notebooks} /> - <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 - name="Monographs" - initialParams={{ - item: { type: "monograph" }, - canGoBack: false, - title: strings.monographs() - }} - component={Monographs} - /> - <NativeStack.Screen name="Notebook" component={NotebookScreen} /> - <NativeStack.Screen name="Search" component={Search} /> - </NativeStack.Navigator> - ); -}; -const Tabs = React.memo(_Tabs, () => true); - -const _NavigationStack = () => { - const clearSelection = useSelectionStore((state) => state.clearSelection); - const loading = useSettingStore((state) => state.isAppLoading); - const onStateChange = React.useCallback(() => { - if (useSelectionStore.getState().selectionMode) { - clearSelection(true); - } - hideAllTooltips(); - eSendEvent("navigate"); - }, [clearSelection]); - - return ( - <Container> - <NavigationContainer onStateChange={onStateChange} ref={rootNavigatorRef}> - <Tabs /> - </NavigationContainer> - {loading ? null : <NotebookSheet />} - </Container> - ); -}; -export const NavigationStack = React.memo(_NavigationStack, () => true); diff --git a/apps/mobile/app/navigation/navigation-stack.tsx b/apps/mobile/app/navigation/navigation-stack.tsx new file mode 100644 index 000000000..7e9137097 --- /dev/null +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -0,0 +1,158 @@ +/* +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 { strings } from "@notesnook/intl"; +import { useThemeColors } from "@notesnook/theme"; +import { NavigationContainer } from "@react-navigation/native"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import * as React from "react"; +import AppLockedScreen from "../components/app-lock-overlay"; +import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets"; +import { hideAllTooltips } from "../hooks/use-tooltip"; +import Favorites from "../screens/favorites"; +import Home from "../screens/home"; +import NotebookScreen from "../screens/notebook"; +import Notebooks from "../screens/notebooks"; +import { ColoredNotes } from "../screens/notes/colored"; +import { Monographs } from "../screens/notes/monographs"; +import { TaggedNotes } from "../screens/notes/tagged"; +import Reminders from "../screens/reminders"; +import { Search } from "../screens/search"; +import Settings from "../screens/settings"; +import Tags from "../screens/tags"; +import Trash from "../screens/trash"; +import SettingsService from "../services/settings"; +import useNavigationStore, { + RouteParams +} from "../stores/use-navigation-store"; +import { useSelectionStore } from "../stores/use-selection-store"; +import { useSettingStore } from "../stores/use-setting-store"; +import { + appNavigatorRef, + fluidTabsRef, + rootNavigatorRef +} from "../utils/global-refs"; +import { FluidPanelsView } from "./fluid-panels-view"; + +const RootStack = createNativeStackNavigator(); +const AppStack = createNativeStackNavigator(); + +const AppNavigation = React.memo( + () => { + const { colors } = useThemeColors(); + const homepage = SettingsService.get().homepage; + const introCompleted = useSettingStore( + (state) => state.settings.introCompleted + ); + const height = useSettingStore((state) => state.dimensions.height); + const insets = useGlobalSafeAreaInsets(); + const screenHeight = height - (50 + insets.top + insets.bottom); + React.useEffect(() => { + setTimeout(() => { + useNavigationStore.getState().update(homepage as keyof RouteParams); + }, 1000); + }, [homepage]); + + return ( + <AppStack.Navigator + initialRouteName={!introCompleted ? "Welcome" : homepage} + screenOptions={{ + headerShown: false, + animation: "none", + contentStyle: { + backgroundColor: colors.primary.background, + minHeight: !introCompleted ? undefined : screenHeight + } + }} + > + <AppStack.Screen name="Notes" component={Home} /> + <AppStack.Screen name="Notebooks" component={Notebooks} /> + <AppStack.Screen name="Favorites" component={Favorites} /> + <AppStack.Screen name="Trash" component={Trash} /> + <AppStack.Screen name="Tags" component={Tags} /> + <AppStack.Screen name="TaggedNotes" component={TaggedNotes} /> + <AppStack.Screen name="ColoredNotes" component={ColoredNotes} /> + <AppStack.Screen name="Reminders" component={Reminders} /> + <AppStack.Screen + name="Monographs" + initialParams={{ + item: { type: "monograph" }, + canGoBack: false, + title: strings.monographs() + }} + component={Monographs} + /> + <AppStack.Screen name="Notebook" component={NotebookScreen} /> + <AppStack.Screen name="Search" component={Search} /> + </AppStack.Navigator> + ); + }, + () => true +); +AppNavigation.displayName = "AppNavigation"; + +export const RootNavigation = () => { + return ( + <NavigationContainer ref={rootNavigatorRef} independent> + <RootStack.Navigator + screenOptions={{ + headerShown: false + }} + screenListeners={{ + focus: (props) => { + if (props.target?.startsWith("FluidPanelsView")) { + fluidTabsRef.current?.unlock(); + } else { + fluidTabsRef.current?.lock(); + } + } + }} + initialRouteName="FluidPanelsView" + > + <RootStack.Screen name="FluidPanelsView" component={FluidPanelsView} /> + <RootStack.Screen name="AppLock" component={AppLockedScreen} /> + <AppStack.Screen name="Settings" component={Settings} /> + </RootStack.Navigator> + </NavigationContainer> + ); +}; + +export const AppNavigationStack = React.memo( + () => { + const clearSelection = useSelectionStore((state) => state.clearSelection); + const onStateChange = React.useCallback(() => { + if (useSelectionStore.getState().selectionMode) { + clearSelection(); + } + hideAllTooltips(); + }, [clearSelection]); + + return ( + <NavigationContainer + independent + onStateChange={onStateChange} + ref={appNavigatorRef} + > + <AppNavigation /> + </NavigationContainer> + ); + }, + () => true +); +AppNavigationStack.displayName = "AppNavigationStack"; diff --git a/apps/mobile/app/navigation/tabs-holder.js b/apps/mobile/app/navigation/tabs-holder.js deleted file mode 100644 index c00e54d7f..000000000 --- a/apps/mobile/app/navigation/tabs-holder.js +++ /dev/null @@ -1,541 +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 { ScopedThemeProvider, useThemeColors } from "@notesnook/theme"; -import { - activateKeepAwake, - deactivateKeepAwake -} from "@sayem314/react-native-keep-awake"; -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { Dimensions, Platform, StatusBar, View } from "react-native"; -import { - addOrientationListener, - addSpecificOrientationListener, - getInitialOrientation, - getSpecificOrientation, - removeOrientationListener, - removeSpecificOrientationListener -} from "react-native-orientation"; -import Animated, { - useAnimatedStyle, - useSharedValue, - withTiming -} from "react-native-reanimated"; -import { notesnook } from "../../e2e/test.ids"; -import { db } from "../common/database"; -import { SideMenu } from "../components/side-menu"; -import { useSideBarDraggingStore } from "../components/side-menu/dragging-store"; -import { FluidTabs } from "../components/tabs"; -import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets"; -import { useShortcutManager } from "../hooks/use-shortcut-manager"; -import { hideAllTooltips } from "../hooks/use-tooltip"; -import { useTabStore } from "../screens/editor/tiptap/use-tab-store"; -import { - clearAppState, - editorController, - editorState, - getAppState -} from "../screens/editor/tiptap/utils"; -import { EditorWrapper } from "../screens/editor/wrapper"; -import { DDS } from "../services/device-detection"; -import { - eSendEvent, - eSubscribeEvent, - eUnSubscribeEvent -} from "../services/event-manager"; -import { useSettingStore } from "../stores/use-setting-store"; -import { - eCloseFullscreenEditor, - eOnEnterEditor, - eOnExitEditor, - eOnLoadNote, - eOpenFullscreenEditor, - eUnlockNote -} from "../utils/events"; -import { editorRef, tabBarRef } from "../utils/global-refs"; -import { sleep } from "../utils/time"; -import { NavigationStack } from "./navigation-stack"; - -const _TabsHolder = () => { - const { colors } = useThemeColors(); - const deviceMode = useSettingStore((state) => state.deviceMode); - const setFullscreen = useSettingStore((state) => state.setFullscreen); - const fullscreen = useSettingStore((state) => state.fullscreen); - const setDeviceModeState = useSettingStore((state) => state.setDeviceMode); - const dimensions = useSettingStore((state) => state.dimensions); - const setDimensions = useSettingStore((state) => state.setDimensions); - const insets = useGlobalSafeAreaInsets(); - const animatedOpacity = useSharedValue(0); - const animatedTranslateY = useSharedValue(-9999); - const overlayRef = useRef(); - const [orientation, setOrientation] = useState(getInitialOrientation()); - - const introCompleted = useSettingStore( - (state) => state.settings.introCompleted - ); - - useShortcutManager({ - onShortcutPressed: async (item) => { - if (!item && getAppState()) { - editorState().movedAway = false; - tabBarRef.current?.goToPage(1, false); - return; - } - if (item && item.type === "notesnook.action.newnote") { - clearAppState(); - if (!tabBarRef.current) { - await sleep(3000); - eSendEvent(eOnLoadNote, { newNote: true }); - editorState().movedAway = false; - tabBarRef.current?.goToPage(1, false); - return; - } - eSendEvent(eOnLoadNote, { newNote: true }); - editorState().movedAway = false; - tabBarRef.current?.goToPage(1, false); - } - } - }); - - const onOrientationChange = (o, o2) => { - setOrientation(o || o2); - }; - - useEffect(() => { - if (Platform.OS === "ios") { - addSpecificOrientationListener(onOrientationChange); - getSpecificOrientation && getSpecificOrientation(onOrientationChange); - } else { - addOrientationListener(onOrientationChange); - } - - return () => { - removeSpecificOrientationListener(onOrientationChange); - removeOrientationListener(onOrientationChange); - }; - }, []); - - const showFullScreenEditor = useCallback(() => { - setFullscreen(true); - if (deviceMode === "smallTablet") { - tabBarRef.current?.openDrawer(false); - } - editorRef.current?.setNativeProps({ - style: { - width: dimensions.width, - zIndex: 999, - paddingHorizontal: - deviceMode === "smallTablet" - ? dimensions.width * 0 - : dimensions.width * 0.15 - } - }); - }, [deviceMode, dimensions.width, setFullscreen]); - - const closeFullScreenEditor = useCallback( - (current) => { - const _deviceMode = current || deviceMode; - if (_deviceMode === "smallTablet") { - tabBarRef.current?.closeDrawer(false); - } - setFullscreen(false); - editorController.current?.commands.updateSettings({ - fullscreen: false - }); - editorRef.current?.setNativeProps({ - style: { - width: - _deviceMode === "smallTablet" - ? dimensions.width - - valueLimiter(dimensions.width * 0.4, 300, 450) - : dimensions.width * 0.48, - zIndex: null, - paddingHorizontal: 0 - } - }); - if (_deviceMode === "smallTablet") { - tabBarRef.current?.goToIndex(1, false); - } - if (_deviceMode === "mobile") { - tabBarRef.current?.goToIndex(2, false); - } - }, - [deviceMode, dimensions.width, setFullscreen] - ); - - useEffect(() => { - if (!tabBarRef.current?.isDrawerOpen()) { - toggleView(false); - } - eSubscribeEvent(eOpenFullscreenEditor, showFullScreenEditor); - eSubscribeEvent(eCloseFullscreenEditor, closeFullScreenEditor); - - return () => { - eUnSubscribeEvent(eOpenFullscreenEditor, showFullScreenEditor); - eUnSubscribeEvent(eCloseFullscreenEditor, closeFullScreenEditor); - }; - }, [ - deviceMode, - dimensions, - colors, - showFullScreenEditor, - closeFullScreenEditor, - toggleView - ]); - - const _onLayout = (event) => { - let size = event?.nativeEvent?.layout; - if (!size || (size.width === dimensions.width && deviceMode !== null)) { - DDS.setSize(size, orientation); - setDeviceMode(deviceMode, size); - checkDeviceType(size); - return; - } - - checkDeviceType(size); - }; - - if (!deviceMode) { - const size = Dimensions.get("window"); - checkDeviceType(size); - } - - function checkDeviceType(size) { - setDimensions({ - width: size.width, - height: size.height - }); - DDS.setSize(size, orientation); - const nextDeviceMode = DDS.isLargeTablet() - ? "tablet" - : DDS.isSmallTab - ? "smallTablet" - : "mobile"; - - setDeviceMode(nextDeviceMode, size); - } - - function setDeviceMode(current, size) { - setDeviceModeState(current); - - let needsUpdate = current !== deviceMode; - - if (fullscreen && current !== "mobile") { - // Runs after size is set via state. - setTimeout(() => { - editorRef.current?.setNativeProps({ - style: { - width: size.width, - zIndex: 999, - paddingHorizontal: - current === "smallTablet" ? size.width * 0 : size.width * 0.15 - } - }); - }, 1); - } else { - if (fullscreen) eSendEvent(eCloseFullscreenEditor, current); - editorRef.current?.setNativeProps({ - style: { - position: "relative", - width: - current === "tablet" - ? size.width * 0.48 - : current === "smallTablet" - ? size.width - valueLimiter(size.width * 0.4, 300, 450) - : size.width, - zIndex: null, - paddingHorizontal: 0 - } - }); - } - if (!needsUpdate) { - return; - } - - const state = getAppState(); - switch (current) { - case "tablet": - tabBarRef.current?.goToIndex(0, false); - break; - case "smallTablet": - if (!fullscreen) { - tabBarRef.current?.closeDrawer(false); - } else { - tabBarRef.current?.openDrawer(false); - } - break; - case "mobile": - if ( - state && - editorState().movedAway === false && - useTabStore.getState().getCurrentNoteId() - ) { - tabBarRef.current?.goToIndex(2, false); - } else { - tabBarRef.current?.goToIndex(1, false); - } - break; - } - } - - const onScroll = (scrollOffset) => { - hideAllTooltips(); - if (scrollOffset > offsets[deviceMode].sidebar - 10) { - animatedOpacity.value = 0; - toggleView(false); - } else { - let o = scrollOffset / 300; - let op = 0; - if (o < 0) { - op = 1; - } else { - op = 1 - o; - } - animatedOpacity.value = op; - toggleView(op < 0.1 ? false : true); - } - }; - - const toggleView = useCallback( - (show) => { - animatedTranslateY.value = show ? 0 : -9999; - }, - [animatedTranslateY] - ); - - const valueLimiter = (value, min, max) => { - if (value < min) { - return min; - } - - if (value > max) { - return max; - } - - return value; - }; - - const offsets = { - mobile: { - sidebar: dimensions.width * 0.75, - list: dimensions.width + dimensions.width * 0.75, - editor: dimensions.width * 2 + dimensions.width * 0.75 - }, - smallTablet: { - sidebar: fullscreen ? 0 : valueLimiter(dimensions.width * 0.3, 300, 350), - list: fullscreen - ? 0 - : dimensions.width + valueLimiter(dimensions.width * 0.3, 300, 350), - editor: fullscreen - ? 0 - : dimensions.width + valueLimiter(dimensions.width * 0.3, 300, 350) - }, - tablet: { - sidebar: 0, - list: 0, - editor: 0 - } - }; - const widths = { - mobile: { - sidebar: dimensions.width * 0.75, - list: dimensions.width, - editor: dimensions.width - }, - smallTablet: { - sidebar: valueLimiter(dimensions.width * 0.3, 300, 350), - list: valueLimiter(dimensions.width * 0.4, 300, 450), - editor: dimensions.width - valueLimiter(dimensions.width * 0.4, 300, 450) - }, - tablet: { - sidebar: dimensions.width * 0.22, - list: dimensions.width * 0.3, - editor: dimensions.width * 0.48 - } - }; - - const animatedStyle = useAnimatedStyle(() => { - return { - opacity: animatedOpacity.value, - transform: [ - { - translateY: animatedTranslateY.value - } - ] - }; - }, []); - - // useEffect(() => { - // changeSystemBarColors(); - // setTimeout(() => { - // changeSystemBarColors(); - // }, 1000); - // }, [colors.primary.background, isDark]); - - return ( - <View - onLayout={_onLayout} - testID={notesnook.ids.default.root} - style={{ - height: "100%", - width: "100%", - backgroundColor: colors.primary.background, - paddingBottom: Platform.OS === "android" ? insets?.bottom : 0, - marginRight: - orientation === "LANDSCAPE-RIGHT" && Platform.OS === "ios" - ? insets.right - : 0, - marginLeft: - orientation === "LANDSCAPE-LEFT" && Platform.OS === "ios" - ? insets.left - : 0 - }} - > - <StatusBar translucent={true} backgroundColor="transparent" /> - - {!introCompleted ? ( - <NavigationStack /> - ) : ( - <> - {deviceMode && widths[deviceMode] ? ( - <FluidTabs - ref={tabBarRef} - dimensions={dimensions} - widths={widths[deviceMode]} - enabled={deviceMode !== "tablet" && !fullscreen} - onScroll={onScroll} - onChangeTab={onChangeTab} - onDrawerStateChange={(state) => { - if (!state) { - useSideBarDraggingStore.setState({ - dragging: false - }); - } - }} - > - <View - key="1" - style={{ - height: "100%", - width: fullscreen ? 0 : widths[deviceMode]?.sidebar - }} - > - <ScopedThemeProvider value="navigationMenu"> - <SideMenu /> - </ScopedThemeProvider> - </View> - - <View - key="2" - style={{ - height: "100%", - width: fullscreen ? 0 : widths[deviceMode]?.list - }} - > - <ScopedThemeProvider value="list"> - {deviceMode === "mobile" ? ( - <Animated.View - onTouchEnd={() => { - if (useSideBarDraggingStore.getState().dragging) { - useSideBarDraggingStore.setState({ - dragging: false - }); - return; - } - tabBarRef.current?.closeDrawer(); - animatedOpacity.value = withTiming(0); - animatedTranslateY.value = withTiming(-9999); - }} - style={[ - { - position: "absolute", - width: "100%", - height: "100%", - zIndex: 999, - backgroundColor: colors.primary.backdrop - }, - animatedStyle - ]} - ref={overlayRef} - /> - ) : null} - - <NavigationStack /> - </ScopedThemeProvider> - </View> - - <ScopedThemeProvider value="editor"> - <EditorWrapper - key="3" - widths={widths} - dimensions={dimensions} - /> - </ScopedThemeProvider> - </FluidTabs> - ) : null} - </> - )} - </View> - ); -}; -export const TabHolder = React.memo(_TabsHolder, () => true); - -const onChangeTab = async (event) => { - if (event.i === 2) { - editorState().movedAway = false; - editorState().isFocused = true; - activateKeepAwake(); - eSendEvent(eOnEnterEditor); - - if (!useTabStore.getState().getCurrentNoteId()) { - eSendEvent(eOnLoadNote, { - newNote: true - }); - } else { - if ( - useTabStore.getState().getTab(useTabStore.getState().currentTab).session - ?.locked - ) { - eSendEvent(eUnlockNote); - } - } - } else { - if (event.from === 2) { - deactivateKeepAwake(); - editorState().movedAway = true; - editorState().isFocused = false; - eSendEvent(eOnExitEditor); - - // Lock all tabs with locked notes... - for (const tab of useTabStore.getState().tabs) { - const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId; - if (!noteId) continue; - const note = await db.notes.note(noteId); - const locked = note && (await db.vaults.itemExists(note)); - if (locked) { - useTabStore.getState().updateTab(tab.id, { - session: { - locked: true - } - }); - } - } - } - } -}; diff --git a/apps/mobile/app/screens/editor/index.tsx b/apps/mobile/app/screens/editor/index.tsx index 0b939244c..c9e844ac7 100755 --- a/apps/mobile/app/screens/editor/index.tsx +++ b/apps/mobile/app/screens/editor/index.tsx @@ -19,8 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. /* eslint-disable @typescript-eslint/no-var-requires */ -import { i18n } from "@lingui/core"; -import { strings } from "@notesnook/intl"; import React, { forwardRef, useCallback, @@ -48,7 +46,6 @@ import { eUnlockWithPassword } from "../../utils/events"; import { openLinkInBrowser } from "../../utils/functions"; -import { tabBarRef } from "../../utils/global-refs"; import EditorOverlay from "./loading"; import { EDITOR_URI } from "./source"; import { EditorProps, useEditorType } from "./tiptap/types"; @@ -61,6 +58,9 @@ import { openInternalLink, randId } from "./tiptap/utils"; +import { fluidTabsRef } from "../../utils/global-refs"; +import { strings } from "@notesnook/intl"; +import { i18n } from "@lingui/core"; const style: ViewStyle = { height: "100%", @@ -348,7 +348,7 @@ const useLockedNoteHandler = () => { }), eSubscribeEvent(eUnlockWithPassword, onSubmit) ]; - if (tabRef.current?.session?.locked && tabBarRef.current?.page() === 2) { + if (tabRef.current?.session?.locked && fluidTabsRef.current?.page() === 2) { unlock(); } return () => { diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx index a18752bd8..ee2ee4df4 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx +++ b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx @@ -73,7 +73,7 @@ import { eUnlockWithPassword } from "../../../utils/events"; import { openLinkInBrowser } from "../../../utils/functions"; -import { tabBarRef } from "../../../utils/global-refs"; +import { fluidTabsRef } from "../../../utils/global-refs"; import { useDragState } from "../../settings/editor/state"; import { EditorMessage, EditorProps, useEditorType } from "./types"; import { useTabStore } from "./use-tab-store"; @@ -259,7 +259,7 @@ export const useEditorEvents = ( if (deviceMode === "mobile") { editorState().movedAway = true; - tabBarRef.current?.goToPage(0); + fluidTabsRef.current?.goToPage(0); } setTimeout(() => { @@ -269,7 +269,7 @@ export const useEditorEvents = ( }, [editor, deviceMode, fullscreen]); const onHardwareBackPress = useCallback(() => { - if (tabBarRef.current?.page() === 2) { + if (fluidTabsRef.current?.page() === 2) { onBackPress(); return true; } diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor.ts b/apps/mobile/app/screens/editor/tiptap/use-editor.ts index 83bd89290..5c71f1ed8 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-editor.ts @@ -61,7 +61,7 @@ import { eShowMergeDialog, eUpdateNoteInEditor } from "../../../utils/events"; -import { tabBarRef } from "../../../utils/global-refs"; +import { fluidTabsRef } from "../../../utils/global-refs"; import { sleep } from "../../../utils/time"; import { unlockVault } from "../../../utils/unlock-vault"; import { onNoteCreated } from "../../notes/common"; @@ -995,14 +995,14 @@ export const useEditor = ( if (!appState) return; state.current.isRestoringState = true; state.current.currentlyEditing = true; - if (tabBarRef.current?.page() === 2) { + if (fluidTabsRef.current?.page() === 2) { state.current.movedAway = false; } if (!state.current.editorStateRestored) { state.current.isRestoringState = true; if (!DDS.isTab) { - tabBarRef.current?.goToPage(1, false); + fluidTabsRef.current?.goToPage(1, false); } } @@ -1081,12 +1081,12 @@ export const useEditor = ( item: note }); } - tabBarRef.current?.goToPage(1); + fluidTabsRef.current?.goToPage(1); } else { noteId = useTabStore.getState().getCurrentNoteId() || null; if (!noteId) { loadNote({ newNote: true }); - if (tabBarRef.current?.page() === 1) { + if (fluidTabsRef.current?.page() === 1) { state.current.currentlyEditing = false; } } else { diff --git a/apps/mobile/app/screens/home/filter-bar.tsx b/apps/mobile/app/screens/home/filter-bar.tsx new file mode 100644 index 000000000..ea05f5de9 --- /dev/null +++ b/apps/mobile/app/screens/home/filter-bar.tsx @@ -0,0 +1,66 @@ +/* +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 from "react"; +import { View } from "react-native"; +import { Button } from "../../components/ui/button"; +import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; + +export const FilterBar = () => { + return ( + <View + style={{ + flexDirection: "row", + alignItems: "center", + gap: 10, + marginTop: DefaultAppStyles.GAP, + width: "93%", + alignSelf: "center" + }} + > + {[ + { + name: "All" + }, + { + name: "Favorites" + } + ].map((item) => ( + <Button + key={item.name} + title={item.name} + type="secondary" + style={{ + height: 25 + }} + /> + ))} + <Button + key="add" + icon="plus" + type="secondary" + iconSize={SIZE.md} + style={{ + height: 25 + }} + /> + </View> + ); +}; diff --git a/apps/mobile/app/screens/home/index.tsx b/apps/mobile/app/screens/home/index.tsx index 303f7c069..f7606b0ff 100755 --- a/apps/mobile/app/screens/home/index.tsx +++ b/apps/mobile/app/screens/home/index.tsx @@ -17,6 +17,7 @@ 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 { strings } from "@notesnook/intl"; import React from "react"; import { FloatingButton } from "../../components/container/floating-button"; import DelayLayout from "../../components/delay-layout"; @@ -29,7 +30,7 @@ import SettingsService from "../../services/settings"; import useNavigationStore from "../../stores/use-navigation-store"; import { useNotes } from "../../stores/use-notes-store"; import { openEditor } from "../notes/common"; -import { strings } from "@notesnook/intl"; +import { FilterBar } from "./filter-bar"; export const Home = ({ navigation, route }: NavigationProps<"Notes">) => { const [notes, loading] = useNotes(); @@ -66,6 +67,9 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => { id={route.name} onPressDefaultRightButton={openEditor} /> + + <FilterBar /> + <DelayLayout wait={loading}> <List data={notes} diff --git a/apps/mobile/app/screens/notebook/index.tsx b/apps/mobile/app/screens/notebook/index.tsx index bc8991bb9..2815d6856 100644 --- a/apps/mobile/app/screens/notebook/index.tsx +++ b/apps/mobile/app/screens/notebook/index.tsx @@ -17,15 +17,17 @@ 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 { resolveItems } from "@notesnook/common"; -import { VirtualizedGrouping } from "@notesnook/core"; -import { Note, Notebook } from "@notesnook/core"; +import { Note, Notebook, VirtualizedGrouping } from "@notesnook/core"; +import { strings } from "@notesnook/intl"; 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 { NotebookHeader } from "../../components/list-items/headers/notebook-header"; +import { Properties } from "../../components/properties"; import SelectionHeader from "../../components/selection-header"; import { AddNotebookSheet } from "../../components/sheets/add-notebook"; import { IconButton } from "../../components/ui/icon-button"; @@ -40,8 +42,8 @@ import useNavigationStore, { import { eUpdateNotebookRoute } from "../../utils/events"; import { findRootNotebookId } from "../../utils/notebooks"; import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; import { openEditor, setOnFirstSave } from "../notes/common"; -import { strings } from "@notesnook/intl"; const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { const [notes, setNotes] = useState<VirtualizedGrouping<Note>>(); @@ -125,6 +127,8 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { setNotes(notes); await notes.item(0, resolveItems); syncWithNavigation(); + } else { + Navigation.goBack(); } setLoading(false); } catch (e) { @@ -160,13 +164,18 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { renderedInRoute={route.name} title={params.current.item?.title} canGoBack={params?.current?.canGoBack} + rightButton={{ + name: "dots-vertical", + onPress: () => { + Properties.present(params.current.item); + } + }} hasSearch={true} onSearch={() => { const selector = db.relations.from( params.current.item, "note" ).selector; - Navigation.push("Search", { placeholder: strings.searchInRoute(params.current.item?.title), type: "note", @@ -175,19 +184,18 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { items: selector }); }} - titleHiddenOnRender id={params.current.item?.id} - onPressDefaultRightButton={openEditor} /> {breadcrumbs && breadcrumbs.length > 0 ? ( <View style={{ width: "100%", - paddingHorizontal: 12, + paddingHorizontal: DefaultAppStyles.GAP, flexDirection: "row", alignItems: "center", - flexWrap: "wrap" + flexWrap: "wrap", + marginTop: DefaultAppStyles.GAP_VERTICAL }} > <IconButton @@ -260,6 +268,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { }} /> </DelayLayout> + <FloatingButton onPress={openEditor} /> </> ); }; diff --git a/apps/mobile/app/screens/notes/common.ts b/apps/mobile/app/screens/notes/common.ts index ec0dd027a..7b0b36c25 100644 --- a/apps/mobile/app/screens/notes/common.ts +++ b/apps/mobile/app/screens/notes/common.ts @@ -27,7 +27,7 @@ import { useRelationStore } from "../../stores/use-relation-store"; import { useTagStore } from "../../stores/use-tag-store"; import { eOnLoadNote, eOnNotebookUpdated } from "../../utils/events"; import { openLinkInBrowser } from "../../utils/functions"; -import { tabBarRef } from "../../utils/global-refs"; +import { fluidTabsRef } from "../../utils/global-refs"; import { editorState } from "../editor/tiptap/utils"; export const PLACEHOLDER_DATA = { @@ -58,7 +58,7 @@ export function openEditor() { eSendEvent(eOnLoadNote, { newNote: true }); editorState().currentlyEditing = true; editorState().movedAway = false; - tabBarRef.current?.goToPage(1); + fluidTabsRef.current?.goToPage(1); } else { eSendEvent(eOnLoadNote, { newNote: true }); } diff --git a/apps/mobile/app/screens/search/search-bar.tsx b/apps/mobile/app/screens/search/search-bar.tsx index e4c85f4a8..2176ef7bc 100644 --- a/apps/mobile/app/screens/search/search-bar.tsx +++ b/apps/mobile/app/screens/search/search-bar.tsx @@ -17,18 +17,18 @@ 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 { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; import React, { useRef } from "react"; import { Platform, 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"; -import { useSelectionStore } from "../../stores/use-selection-store"; +import Navigation from "../../services/navigation"; import useNavigationStore from "../../stores/use-navigation-store"; -import { strings } from "@notesnook/intl"; +import { useSelectionStore } from "../../stores/use-selection-store"; +import { SIZE } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; export const SearchBar = ({ onChangeText, loading @@ -50,52 +50,56 @@ export const SearchBar = ({ return selectionMode && isFocused ? null : ( <View style={{ - height: Platform.OS === "android" ? 50 + insets.top : 50, - paddingTop: Platform.OS === "ios" ? 0 : insets.top, - flexDirection: "row", - alignItems: "center", - flexShrink: 1, width: "100%", - paddingHorizontal: 12 + paddingHorizontal: DefaultAppStyles.GAP, + paddingTop: Platform.OS === "ios" ? 0 : insets.top + 5 }} > - <IconButton - name="arrow-left" - size={SIZE.xxl} - top={10} - bottom={10} - onPress={() => { - Navigation.goBack(); - }} - color={colors.primary.paragraph} - type="plain" + <View style={{ - paddingLeft: 0, - marginLeft: -5, - marginRight: DDS.isLargeTablet() ? 10 : 7 + flexDirection: "row", + alignItems: "center", + width: "100%", + paddingHorizontal: DefaultAppStyles.GAP_SMALL, + paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL, + borderRadius: 10, + borderColor: colors.primary.border, + borderWidth: 1 }} - /> + > + <IconButton + name="arrow-left" + size={SIZE.xxl} + top={10} + bottom={10} + onPress={() => { + Navigation.goBack(); + }} + color={colors.primary.paragraph} + type="plain" + /> - <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={strings.typeAKeyword()} - textContentType="none" - returnKeyLabel={strings.search()} - returnKeyType="search" - autoCapitalize="none" - autoCorrect={false} - placeholderTextColor={colors.primary.placeholder} - /> + <TextInput + ref={inputRef} + testID="search-input" + style={{ + fontSize: SIZE.sm, + fontFamily: "OpenSans-Regular", + flexGrow: 1, + color: colors.primary.paragraph, + height: 40 + }} + autoFocus + onChangeText={_onChangeText} + placeholder={strings.typeAKeyword()} + textContentType="none" + returnKeyLabel={strings.search()} + returnKeyType="search" + autoCapitalize="none" + autoCorrect={false} + placeholderTextColor={colors.primary.placeholder} + /> + </View> </View> ); }; diff --git a/apps/mobile/app/screens/settings/group.tsx b/apps/mobile/app/screens/settings/group.tsx index 422ce7837..bdb6f46e3 100644 --- a/apps/mobile/app/screens/settings/group.tsx +++ b/apps/mobile/app/screens/settings/group.tsx @@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { NativeStackScreenProps } from "@react-navigation/native-stack"; -import React, { useEffect } from "react"; +import React from "react"; import { View } from "react-native"; import { KeyboardAwareFlatList } from "react-native-keyboard-aware-scroll-view"; import Animated, { FadeInDown } from "react-native-reanimated"; @@ -26,7 +26,6 @@ import DelayLayout from "../../components/delay-layout"; import { Header } from "../../components/header"; import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import useNavigationStore from "../../stores/use-navigation-store"; -import { tabBarRef } from "../../utils/global-refs"; import { components } from "./components"; import { SectionItem } from "./section-item"; import { RouteParams, SettingSection } from "./types"; @@ -42,16 +41,10 @@ const Group = ({ }: NativeStackScreenProps<RouteParams, "SettingsGroup">) => { useNavigationFocus(navigation, { onFocus: () => { - tabBarRef.current?.lock(); useNavigationStore.getState().setFocusedRouteId("Settings"); return false; } }); - useEffect(() => { - return () => { - tabBarRef.current?.unlock(); - }; - }, []); const renderItem = ({ item }: { item: SettingSection; index: number }) => ( <SectionItem item={item} /> ); diff --git a/apps/mobile/app/screens/settings/home.tsx b/apps/mobile/app/screens/settings/home.tsx index 1ad42893a..0c273e222 100644 --- a/apps/mobile/app/screens/settings/home.tsx +++ b/apps/mobile/app/screens/settings/home.tsx @@ -56,7 +56,8 @@ const Home = ({ <Header renderedInRoute="Settings" title={strings.routes.Settings()} - canGoBack={false} + canGoBack={true} + hasSearch={false} id="Settings" /> <DelayLayout delay={300} type="settings"> diff --git a/apps/mobile/app/screens/settings/index.tsx b/apps/mobile/app/screens/settings/index.tsx index c9d62393c..6aec7bf2b 100644 --- a/apps/mobile/app/screens/settings/index.tsx +++ b/apps/mobile/app/screens/settings/index.tsx @@ -24,31 +24,39 @@ import { useThemeColors } from "@notesnook/theme"; import Group from "./group"; import Home from "./home"; import { RouteParams } from "./types"; +import { SafeAreaView } from "react-native-safe-area-context"; const SettingsStack = createNativeStackNavigator<RouteParams>(); export const Settings = () => { const { colors } = useThemeColors(); return ( - <SettingsStack.Navigator - initialRouteName="SettingsHome" - screenListeners={{ - focus: (e) => { - if (e.target?.startsWith("SettingsHome-")) { - useNavigationStore.getState().update("Settings"); - } - } - }} - screenOptions={{ - animation: "none", - headerShown: false, - contentStyle: { - backgroundColor: colors.primary.background - } + <SafeAreaView + style={{ + flex: 1, + backgroundColor: colors.primary.background }} > - <SettingsStack.Screen name="SettingsHome" component={Home} /> - <SettingsStack.Screen name="SettingsGroup" component={Group} /> - </SettingsStack.Navigator> + <SettingsStack.Navigator + initialRouteName="SettingsHome" + screenListeners={{ + focus: (e) => { + if (e.target?.startsWith("SettingsHome-")) { + useNavigationStore.getState().update("Settings"); + } + } + }} + screenOptions={{ + animation: "none", + headerShown: false, + contentStyle: { + backgroundColor: colors.primary.background + } + }} + > + <SettingsStack.Screen name="SettingsHome" component={Home} /> + <SettingsStack.Screen name="SettingsGroup" component={Group} /> + </SettingsStack.Navigator> + </SafeAreaView> ); }; diff --git a/apps/mobile/app/services/navigation.ts b/apps/mobile/app/services/navigation.ts index 2394e0699..7e1e0a831 100755 --- a/apps/mobile/app/services/navigation.ts +++ b/apps/mobile/app/services/navigation.ts @@ -31,7 +31,7 @@ import { useReminderStore } from "../stores/use-reminder-store"; import { useTagStore } from "../stores/use-tag-store"; import { useTrashStore } from "../stores/use-trash-store"; import { eOnRefreshSearch, eUpdateNotebookRoute } from "../utils/events"; -import { rootNavigatorRef, tabBarRef } from "../utils/global-refs"; +import { appNavigatorRef, fluidTabsRef } from "../utils/global-refs"; import { eSendEvent } from "./event-manager"; /** @@ -122,30 +122,36 @@ function queueRoutesForUpdate(...routesToUpdate: RouteName[]) { } function navigate<T extends RouteName>(screen: T, params?: RouteParams[T]) { - rootNavigatorRef.current?.navigate(screen as any, params); + console.log(`Navigation.navigate ${screen} route`); + appNavigatorRef.current?.navigate(screen as any, params); } function goBack() { - rootNavigatorRef.current?.goBack(); + appNavigatorRef.current?.goBack(); } function push<T extends RouteName>(screen: T, params: RouteParams[T]) { - rootNavigatorRef.current?.dispatch(StackActions.push(screen as any, params)); + console.log(`Navigation.push ${screen} route`); + appNavigatorRef.current?.dispatch(StackActions.push(screen as any, params)); } function replace<T extends RouteName>(screen: T, params: RouteParams[T]) { - rootNavigatorRef.current?.dispatch(StackActions.replace(screen, params)); + console.log(`Navigation.replace ${screen} route`); + appNavigatorRef.current?.dispatch( + StackActions.replace(screen as string, params) + ); } function popToTop() { - rootNavigatorRef.current?.dispatch(StackActions.popToTop()); + console.log(`Navigation.popToTop`); + appNavigatorRef.current?.dispatch(StackActions.popToTop()); } function openDrawer() { - tabBarRef.current?.openDrawer(); + fluidTabsRef.current?.openDrawer(); } function closeDrawer() { - tabBarRef.current?.closeDrawer(); + fluidTabsRef.current?.closeDrawer(); } const Navigation = { diff --git a/apps/mobile/app/services/notifications.ts b/apps/mobile/app/services/notifications.ts index f9be02007..a44ddd70b 100644 --- a/apps/mobile/app/services/notifications.ts +++ b/apps/mobile/app/services/notifications.ts @@ -44,7 +44,7 @@ import { useReminderStore } from "../stores/use-reminder-store"; import { useSettingStore } from "../stores/use-setting-store"; import { useUserStore } from "../stores/use-user-store"; import { eOnLoadNote } from "../utils/events"; -import { tabBarRef } from "../utils/global-refs"; +import { fluidTabsRef } from "../utils/global-refs"; import { convertNoteToText } from "../utils/note-to-text"; import { NotesnookModule } from "../utils/notesnook-module"; import { sleep } from "../utils/time"; @@ -440,7 +440,7 @@ async function loadNote(id: string, jump: boolean) { const note = await db.notes.note(id); if (!note) return; if (!DDS.isTab && jump) { - tabBarRef.current?.goToPage(1); + fluidTabsRef.current?.goToPage(1); } NotesnookModule.setAppState( JSON.stringify({ diff --git a/apps/mobile/app/stores/create-notebook-tree-stores.ts b/apps/mobile/app/stores/create-notebook-tree-stores.ts new file mode 100644 index 000000000..6065453b7 --- /dev/null +++ b/apps/mobile/app/stores/create-notebook-tree-stores.ts @@ -0,0 +1,165 @@ +/* +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 } from "@notesnook/core"; +import create from "zustand"; +import { db } from "../common/database"; +import { createItemSelectionStore } from "./item-selection-store"; + +export type TreeItem = { + parentId: string; + notebook: Notebook; + depth: number; +}; + +function removeTreeItem(tree: TreeItem[], id: string) { + const children: TreeItem[] = []; + let newTree = tree.filter((item) => { + if (item.parentId === id) children.push(item); + return item.notebook.id !== id; + }); + for (const child of children) { + newTree = removeTreeItem(newTree, child.notebook.id); + } + return newTree; +} + +export function createNotebookTreeStores( + multiSelect: boolean, + selectionEnabled: boolean +) { + const useSideMenuNotebookTreeStore = create<{ + tree: TreeItem[]; + setTree: (tree: TreeItem[]) => void; + removeItem: (id: string) => void; + addNotebooks: ( + parentId: string, + notebooks: Notebook[], + depth: number + ) => void; + updateItem: (id: string, notebook: Notebook) => void; + fetchAndAdd: (parentId: string, depth: number) => Promise<void>; + removeChildren: (id: string) => void; + }>((set, get) => ({ + tree: [], + setTree(tree) { + set({ tree }); + }, + updateItem: (id, notebook) => { + const newTree = [...get().tree]; + const index = newTree.findIndex((item) => item.notebook.id === id); + newTree[index] = { + ...newTree[index], + notebook + }; + set({ + tree: newTree + }); + }, + addNotebooks: (parentId: string, notebooks: Notebook[], depth: number) => { + const parentIndex = get().tree.findIndex( + (item) => item.notebook.id === parentId + ); + + let newTree = get().tree.slice(); + for (const item of newTree) { + if (item.parentId === parentId) { + newTree = removeTreeItem(newTree, item.notebook.id); + } + } + newTree.splice( + parentIndex + 1, + 0, + ...notebooks.map((notebook) => { + return { + parentId, + notebook, + depth: depth + }; + }) + ); + set({ + tree: newTree + }); + }, + + removeItem(id) { + set({ + tree: removeTreeItem(get().tree, id).slice() + }); + }, + fetchAndAdd: async (parentId: string, depth: number) => { + const selector = db.relations.from( + { + type: "notebook", + id: parentId + }, + "notebook" + ).selector; + const grouped = await selector.sorted( + db.settings.getGroupOptions("notebooks") + ); + + const notebooks: Notebook[] = []; + for (let index = 0; index < grouped.placeholders.length; index++) { + const notebook = (await grouped.item(index)).item; + if (notebook) notebooks.push(notebook); + } + get().addNotebooks(parentId, notebooks, depth); + }, + removeChildren(id: string) { + let newTree = get().tree.slice(); + for (const item of newTree) { + if (item.parentId === id) { + newTree = removeTreeItem(newTree, item.notebook.id); + } + } + set({ + tree: newTree + }); + } + })); + + const useSideMenuNotebookSelectionStore = createItemSelectionStore( + multiSelect, + selectionEnabled + ); + + const useSideMenuNotebookExpandedStore = create<{ + expanded: { + [id: string]: boolean; + }; + setExpanded: (id: string) => void; + }>((set, get) => ({ + expanded: {}, + setExpanded(id: string) { + set({ + expanded: { + ...get().expanded, + [id]: !get().expanded[id] + } + }); + } + })); + + return { + useSideMenuNotebookTreeStore, + useSideMenuNotebookSelectionStore, + useSideMenuNotebookExpandedStore + }; +} diff --git a/apps/mobile/app/stores/item-selection-store.ts b/apps/mobile/app/stores/item-selection-store.ts index cdde97dfc..a5a63fc03 100644 --- a/apps/mobile/app/stores/item-selection-store.ts +++ b/apps/mobile/app/stores/item-selection-store.ts @@ -33,6 +33,7 @@ export interface SelectionStore extends State { enabled?: boolean; getSelectedItemIds: () => string[]; getDeselectedItemIds: () => string[]; + selectAll?: () => void; } export function createItemSelectionStore( diff --git a/apps/mobile/app/stores/use-navigation-store.ts b/apps/mobile/app/stores/use-navigation-store.ts index 55fb39500..55c47d56c 100644 --- a/apps/mobile/app/stores/use-navigation-store.ts +++ b/apps/mobile/app/stores/use-navigation-store.ts @@ -29,6 +29,7 @@ import { TrashItem } from "@notesnook/core"; import create, { State } from "zustand"; +import { ParamListBase } from "@react-navigation/core"; export type GenericRouteParam = undefined; @@ -55,7 +56,7 @@ export type AuthParams = { canGoBack?: boolean; }; -export type RouteParams = { +export interface RouteParams extends ParamListBase { Notes: GenericRouteParam; Notebooks: { canGoBack?: boolean; @@ -80,7 +81,7 @@ export type RouteParams = { AppLock: AppLockRouteParams; Reminders: GenericRouteParam; SettingsGroup: GenericRouteParam; -}; +} export type RouteName = keyof RouteParams; diff --git a/apps/mobile/app/stores/use-setting-store.ts b/apps/mobile/app/stores/use-setting-store.ts index aa45ceda4..b010c6bdc 100644 --- a/apps/mobile/app/stores/use-setting-store.ts +++ b/apps/mobile/app/stores/use-setting-store.ts @@ -114,7 +114,7 @@ export interface SettingStore extends State { dimensions: DimensionsType; setSettings: (settings: Settings) => void; setFullscreen: (fullscreen: boolean) => void; - setDeviceMode: (mode: string) => void; + setDeviceMode: (mode: string | null) => void; setDimensions: (dimensions: DimensionsType) => void; isAppLoading: boolean; setAppLoading: (isAppLoading: boolean) => void; diff --git a/apps/mobile/app/utils/functions.ts b/apps/mobile/app/utils/functions.ts index 39eb38cf1..c58a6b1a5 100644 --- a/apps/mobile/app/utils/functions.ts +++ b/apps/mobile/app/utils/functions.ts @@ -25,6 +25,7 @@ import { presentDialog } from "../components/dialog/functions"; import { eSendEvent, ToastManager } from "../services/event-manager"; import Navigation from "../services/navigation"; import { useMenuStore } from "../stores/use-menu-store"; +import { useNotebookStore } from "../stores/use-notebook-store"; import { useRelationStore } from "../stores/use-relation-store"; import { useTagStore } from "../stores/use-tag-store"; import { eOnNotebookUpdated, eUpdateNoteInEditor } from "./events"; @@ -72,8 +73,10 @@ async function deleteNotebook(id: string, deleteNotes: boolean) { } } await db.notebooks.moveToTrash(id); - if (parentId) { - eSendEvent(eOnNotebookUpdated, parentId); + + eSendEvent(eOnNotebookUpdated, parentId); + if (!parentId) { + useNotebookStore.getState().refresh(); } } @@ -113,7 +116,6 @@ export const deleteItems = async ( if (!result.delete) return; for (const id of itemIds) { await deleteNotebook(id, result.deleteNotes); - eSendEvent(eOnNotebookUpdated, await getParentNotebookId(id)); } } else if (type === "tag") { presentDialog({ diff --git a/apps/mobile/app/utils/global-refs.ts b/apps/mobile/app/utils/global-refs.ts index f329e9cd3..4c3c9c7e1 100644 --- a/apps/mobile/app/utils/global-refs.ts +++ b/apps/mobile/app/utils/global-refs.ts @@ -20,10 +20,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import { createNavigationContainerRef } from "@react-navigation/native"; import { createRef } from "react"; import { TextInput, View } from "react-native"; -import { TabsRef } from "../components/tabs"; +import { TabsRef } from "../components/fluid-panels"; import { RouteParams } from "../stores/use-navigation-store"; export const inputRef = createRef<TextInput>(); -export const rootNavigatorRef = createNavigationContainerRef<RouteParams>(); -export const tabBarRef = createRef<TabsRef>(); +export const rootNavigatorRef = createNavigationContainerRef<{ + FluidPanelsView: undefined; + AppLock: undefined; + Settings: undefined; +}>(); +export const appNavigatorRef = createNavigationContainerRef<RouteParams>(); +export const fluidTabsRef = createRef<TabsRef>(); export const editorRef = createRef<View>(); diff --git a/apps/mobile/app/utils/menu-items.ts b/apps/mobile/app/utils/menu-items.ts index 5d6fe6303..793d44be4 100644 --- a/apps/mobile/app/utils/menu-items.ts +++ b/apps/mobile/app/utils/menu-items.ts @@ -17,51 +17,69 @@ 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, ItemType } from "@notesnook/core"; import { Monographs } from "../screens/notes/monographs"; -export const MenuItemsList = [ + +export type SideMenuItem = { + dataType?: ItemType | "monograph"; + data?: Item; + title: string; + id: string; + icon: string; + onPress?: (item: SideMenuItem) => void; + onLongPress?: (item: SideMenuItem) => void; +}; + +export const MenuItemsList: SideMenuItem[] = [ { - id: "notes", - name: "Notes", - icon: "home-variant-outline", - close: true + dataType: "note", + id: "Notes", + title: "Notes", + icon: "note-outline" + }, + // { + // dataType: "notebook", + // id: "Notebooks", + // title: "Notebooks", + // icon: "book-outline" + // }, + { + dataType: "note", + id: "Favorites", + title: "Favorites", + icon: "star-outline" + }, + // { + // dataType: "tag", + // id: "Tags", + // title: "Tags", + // icon: "pound" + // }, + { + dataType: "reminder", + id: "Reminders", + title: "Reminders", + icon: "bell" }, { - id: "notebooks", - name: "Notebooks", - icon: "book-outline", - close: true - }, - { - id: "favorites", - name: "Favorites", - icon: "star-outline", - close: true - }, - { - id: "tags", - name: "Tags", - icon: "pound", - close: true - }, - { - id: "reminders", - name: "Reminders", - icon: "bell", - close: true - }, - { - id: "monographs", - name: "Monographs", + dataType: "monograph", + id: "Monographs", + title: "Monographs", icon: "text-box-multiple-outline", - close: true, - func: () => { + onPress: () => { Monographs.navigate(); } }, { - id: "trash", - name: "Trash", - icon: "delete-outline", - close: true + dataType: "note", + id: "Archive", + title: "Archive", + icon: "archive" + }, + { + dataType: "note", + id: "Trash", + title: "Trash", + icon: "delete-outline" } ]; diff --git a/apps/mobile/app/utils/notebooks.ts b/apps/mobile/app/utils/notebooks.ts index c71ac6d8d..d24186b97 100644 --- a/apps/mobile/app/utils/notebooks.ts +++ b/apps/mobile/app/utils/notebooks.ts @@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { db } from "../common/database"; import { eSendEvent } from "../services/event-manager"; +import { useNotebookStore } from "../stores/use-notebook-store"; import { eOnNotebookUpdated } from "./events"; export async function findRootNotebookId(id: string) { @@ -51,9 +52,14 @@ export async function getParentNotebookId(id: string) { return relation?.[0]?.fromId; } -export async function updateNotebook(id?: string) { +export async function updateNotebook(id?: string, updateParent?: boolean) { eSendEvent(eOnNotebookUpdated, id); - if (id) { - eSendEvent(eOnNotebookUpdated, await getParentNotebookId(id)); + if (updateParent && id) { + const parent = await getParentNotebookId(id); + if (parent) { + eSendEvent(eOnNotebookUpdated, parent); + } else { + useNotebookStore.getState().refresh(); + } } } diff --git a/apps/mobile/app/utils/size/index.js b/apps/mobile/app/utils/size/index.js index 08077c5ee..da3d77e46 100644 --- a/apps/mobile/app/utils/size/index.js +++ b/apps/mobile/app/utils/size/index.js @@ -81,31 +81,33 @@ export const normalize = (size) => { return correction(size, 1); } }; -export const SIZE = { - xxs: normalize(11) * scale.fontScale, - xs: normalize(12.5) * scale.fontScale, - sm: normalize(15) * scale.fontScale, - md: normalize(16.5) * scale.fontScale, - lg: normalize(22) * scale.fontScale, - xl: normalize(24) * scale.fontScale, - xxl: normalize(28) * scale.fontScale, - xxxl: normalize(32) * scale.fontScale -}; + +function getSize() { + return { + xxxs: normalize(11) * scale.fontScale, + xxs: normalize(12.5) * scale.fontScale, + xs: normalize(14) * scale.fontScale, + sm: normalize(15) * scale.fontScale, + md: normalize(16.5) * scale.fontScale, + lg: normalize(22) * scale.fontScale, + xl: normalize(24) * scale.fontScale, + xxl: normalize(28) * scale.fontScale, + xxxl: normalize(32) * scale.fontScale + }; +} + +export const SIZE = getSize(); export function updateSize() { - SIZE.xxs = normalize(11) * scale.fontScale; - SIZE.xs = normalize(12.5) * scale.fontScale; - SIZE.sm = normalize(15) * scale.fontScale; - SIZE.md = normalize(16.5) * scale.fontScale; - SIZE.lg = normalize(22) * scale.fontScale; - SIZE.xl = normalize(24) * scale.fontScale; - SIZE.xxl = normalize(28) * scale.fontScale; - SIZE.xxxl = normalize(32) * scale.fontScale; + const newSize = getSize(); + for (const key in SIZE) { + SIZE[key] = newSize[key]; + } ph = normalize(10) * scale.fontScale; pv = normalize(10) * scale.fontScale; } -export const br = 5; // border radius +export const br = 8; // border radius export var ph = normalize(10); // padding horizontal export var pv = normalize(10); // padding vertical export const opacity = 0.5; // active opacity diff --git a/apps/mobile/app/navigation/index.tsx b/apps/mobile/app/utils/styles.ts similarity index 59% rename from apps/mobile/app/navigation/index.tsx rename to apps/mobile/app/utils/styles.ts index 2c5e858af..795b06f25 100644 --- a/apps/mobile/app/navigation/index.tsx +++ b/apps/mobile/app/utils/styles.ts @@ -16,21 +16,9 @@ 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 from "react"; -import DialogProvider from "../components/dialog-provider"; -import { Toast } from "../components/toast"; -import { TabHolder } from "./tabs-holder"; -import { ScopedThemeProvider } from "@notesnook/theme"; -const _ApplicationHolder = () => { - return ( - <> - <TabHolder /> - <ScopedThemeProvider value="dialog"> - <Toast /> - </ScopedThemeProvider> - <DialogProvider /> - </> - ); +export const DefaultAppStyles = { + GAP: 16, + GAP_SMALL: 8, + GAP_VERTICAL: 6, + GAP_VERTICAL_SMALL: 4 }; -export const ApplicationHolder = React.memo(_ApplicationHolder, () => true); diff --git a/apps/mobile/native/fonts/MaterialCommunityIcons.ttf b/apps/mobile/native/fonts/MaterialCommunityIcons.ttf index d2ae55da1..c0fa3e2f8 100644 Binary files a/apps/mobile/native/fonts/MaterialCommunityIcons.ttf and b/apps/mobile/native/fonts/MaterialCommunityIcons.ttf differ diff --git a/apps/mobile/native/globals.js b/apps/mobile/native/globals.js index 072001385..48e263b40 100644 --- a/apps/mobile/native/globals.js +++ b/apps/mobile/native/globals.js @@ -24,7 +24,7 @@ import { } from "@notesnook/intl/dist/locales/$pseudo-LOCALE.json"; i18n.load({ - en: __DEV__ ? $pseudo : $en + en: !__DEV__ ? $pseudo : $en }); setI18nGlobal(i18n); i18n.activate("en"); diff --git a/apps/mobile/native/package.json b/apps/mobile/native/package.json index 59b262e39..e07cf7408 100644 --- a/apps/mobile/native/package.json +++ b/apps/mobile/native/package.json @@ -73,7 +73,9 @@ "react-native-begin-background-task": "github:blockfirm/react-native-begin-background-task", "react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot", "@ammarahmed/react-native-fingerprint-scanner": "^5.0.0", - "@ammarahmed/react-native-share-extension": "^2.9.0" + "@ammarahmed/react-native-share-extension": "^2.9.0", + "react-native-pager-view": "^6.5.1", + "react-native-tab-view": "^4.0.2" }, "devDependencies": { "detox": "^20.27.6", @@ -88,6 +90,7 @@ "@tsconfig/react-native": "^3.0.2", "@types/html-to-text": "^8.0.1", "@types/metro-config": "^0.76.3", + "@types/react": "^18.2.39", "@types/react-native": "^0.69.1", "@types/react-native-vector-icons": "^6.4.10", "@types/react-test-renderer": "^18.0.0", diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index d8682037a..1fd9e1808 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notesnook/mobile", - "version": "3.0.31", + "version": "3.0.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@notesnook/mobile", - "version": "3.0.31", + "version": "3.0.32", "hasInstallScript": true, "license": "GPL-3.0-or-later", "workspaces": [ @@ -14,7 +14,6 @@ "app/" ], "dependencies": { - "@ammarahmed/react-native-share-extension": "^2.9.0", "@notesnook/common": "file:../../packages/common", "@notesnook/core": "file:../../packages/core", "@notesnook/crypto": "file:../../packages/crypto", @@ -3728,6 +3727,7 @@ "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { + "@notesnook-importer/core": "^2.1.1", "@notesnook/common": "file:../common", "@notesnook/intl": "file:../intl", "@notesnook/theme": "file:../theme", @@ -7743,7 +7743,7 @@ }, "../../packages/editor-mobile/node_modules/@types/prop-types": { "version": "15.7.11", - "devOptional": true, + "dev": true, "license": "MIT" }, "../../packages/editor-mobile/node_modules/@types/q": { @@ -7763,7 +7763,7 @@ }, "../../packages/editor-mobile/node_modules/@types/react": { "version": "18.2.39", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -7794,7 +7794,7 @@ }, "../../packages/editor-mobile/node_modules/@types/scheduler": { "version": "0.16.8", - "devOptional": true, + "dev": true, "license": "MIT" }, "../../packages/editor-mobile/node_modules/@types/semver": { @@ -12619,7 +12619,7 @@ }, "../../packages/editor-mobile/node_modules/immer": { "version": "9.0.21", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -23091,6 +23091,7 @@ }, "../../packages/editor/node_modules/js-tokens": { "version": "4.0.0", + "dev": true, "license": "MIT" }, "../../packages/editor/node_modules/jsesc": { @@ -23141,6 +23142,7 @@ }, "../../packages/editor/node_modules/loose-envify": { "version": "1.4.0", + "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -23652,6 +23654,7 @@ }, "../../packages/editor/node_modules/react": { "version": "18.3.1", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -23670,6 +23673,7 @@ }, "../../packages/editor/node_modules/react-dom": { "version": "18.3.1", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -23808,6 +23812,7 @@ }, "../../packages/editor/node_modules/scheduler": { "version": "0.23.2", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -28315,6 +28320,7 @@ "@ammarahmed/react-native-background-fetch": "^4.2.2", "@ammarahmed/react-native-eventsource": "1.1.0", "@ammarahmed/react-native-fingerprint-scanner": "^5.0.0", + "@ammarahmed/react-native-share-extension": "^2.9.0", "@ammarahmed/react-native-sodium": "^1.6.1", "@bam.tech/react-native-image-resizer": "3.0.5", "@callstack/repack": "^4.1.1", @@ -28357,6 +28363,7 @@ "react-native-navigation-bar-color": "2.0.2", "react-native-notification-sounds": "0.5.5", "react-native-orientation": "github:yamill/react-native-orientation", + "react-native-pager-view": "^6.5.1", "react-native-pdf": "6.6.2", "react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot", "react-native-quick-sqlite": "^8.0.6", @@ -28369,6 +28376,7 @@ "react-native-share": "^12.0.3", "react-native-svg": "^12.3.0", "react-native-swiper-flatlist": "3.2.2", + "react-native-tab-view": "^4.0.2", "react-native-theme-switch-animation": "^0.6.0", "react-native-tooltips": "^1.0.3", "react-native-url-polyfill": "^2.0.0", @@ -28390,6 +28398,7 @@ "@types/html-to-text": "^8.0.1", "@types/jest": "^29.5.14", "@types/metro-config": "^0.76.3", + "@types/react": "^18.2.39", "@types/react-native": "^0.69.1", "@types/react-native-vector-icons": "^6.4.10", "@types/react-test-renderer": "^18.0.0", @@ -28590,6 +28599,7 @@ }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { "version": "7.22.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" @@ -28709,6 +28719,7 @@ }, "node_modules/@babel/helper-hoist-variables": { "version": "7.22.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" @@ -28968,6 +28979,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, @@ -28982,6 +28994,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -29130,6 +29143,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, "engines": { "node": ">=6.9.0" }, @@ -29142,6 +29156,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", + "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -29176,6 +29191,7 @@ }, "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" @@ -29188,6 +29204,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -29225,6 +29242,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" }, @@ -29249,6 +29267,7 @@ "version": "7.25.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz", "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -29263,6 +29282,7 @@ "version": "7.25.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -29277,6 +29297,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -29288,6 +29309,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -29387,6 +29409,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -29414,6 +29437,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -29442,6 +29466,7 @@ "version": "7.25.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz", "integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==", + "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", @@ -29472,6 +29497,7 @@ }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.22.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -29500,6 +29526,7 @@ "version": "7.25.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", + "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" @@ -29515,6 +29542,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", @@ -29579,6 +29607,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" @@ -29594,6 +29623,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -29608,6 +29638,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, @@ -29620,6 +29651,7 @@ }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.22.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", @@ -29636,6 +29668,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -29663,6 +29696,7 @@ }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.22.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -29693,6 +29727,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-json-strings": "^7.8.3" @@ -29721,6 +29756,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" @@ -29734,6 +29770,7 @@ }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.22.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -29749,6 +29786,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" @@ -29779,6 +29817,7 @@ "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", + "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-module-transforms": "^7.22.5", @@ -29796,6 +29835,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" @@ -29825,6 +29865,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -29854,6 +29895,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -29869,6 +29911,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "dev": true, "dependencies": { "@babel/compat-data": "^7.22.5", "@babel/helper-compilation-targets": "^7.22.5", @@ -29885,6 +29928,7 @@ }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.22.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -29901,6 +29945,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" @@ -29974,6 +30019,7 @@ }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.22.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -30043,6 +30089,7 @@ }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.22.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -30059,6 +30106,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -30151,6 +30199,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -30181,6 +30230,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -30195,6 +30245,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" @@ -30224,6 +30275,7 @@ "version": "7.25.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", + "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" @@ -30239,6 +30291,7 @@ "version": "7.25.4", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz", "integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==", + "dev": true, "dependencies": { "@babel/compat-data": "^7.22.5", "@babel/helper-compilation-targets": "^7.22.5", @@ -30332,6 +30385,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", @@ -30347,6 +30401,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -33650,7 +33705,8 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "node_modules/@types/graceful-fs": { "version": "4.1.6", @@ -33756,14 +33812,14 @@ }, "node_modules/@types/prop-types": { "version": "15.7.5", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -34119,6 +34175,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -34127,22 +34184,26 @@ "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", @@ -34152,12 +34213,14 @@ "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.11.6", "@webassemblyjs/helper-buffer": "1.11.6", @@ -34169,6 +34232,7 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -34177,6 +34241,7 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -34184,12 +34249,14 @@ "node_modules/@webassemblyjs/utf8": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.11.6", "@webassemblyjs/helper-buffer": "1.11.6", @@ -34205,6 +34272,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", @@ -34217,6 +34285,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.11.6", "@webassemblyjs/helper-buffer": "1.11.6", @@ -34228,6 +34297,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", @@ -34241,6 +34311,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.11.6", "@xtuc/long": "4.2.2" @@ -34298,12 +34369,14 @@ "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", @@ -35302,6 +35375,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, "engines": { "node": ">=6.0" } @@ -35732,7 +35806,7 @@ }, "node_modules/csstype": { "version": "3.1.2", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/date-fns": { @@ -36332,6 +36406,7 @@ "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -36451,7 +36526,8 @@ "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==" + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true }, "node_modules/es-object-atoms": { "version": "1.0.0", @@ -36796,6 +36872,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -36808,6 +36885,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, "engines": { "node": ">=4.0" } @@ -36971,6 +37049,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -36982,6 +37061,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "engines": { "node": ">=4.0" } @@ -36990,6 +37070,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -37749,7 +37830,8 @@ "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true }, "node_modules/global": { "version": "4.4.0", @@ -39838,7 +39920,8 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json-schema-ref-resolver": { "version": "1.0.1", @@ -40314,6 +40397,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, "engines": { "node": ">=6.11.5" } @@ -42543,6 +42627,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -42564,30 +42649,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-dom/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/react-freeze": { "version": "1.0.3", "license": "MIT", @@ -43023,6 +43084,15 @@ "react-native": ">=0.40" } }, + "node_modules/react-native-pager-view": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.5.1.tgz", + "integrity": "sha512-YdX7bP+rPYvATMU7HzlMq9JaG3ui/+cVRbFZFGW+QshDULANFg9ECR1BA7H7JTIcO/ZgWCwF+1aVmYG5yBA9Og==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-pdf": { "version": "6.6.2", "license": "MIT", @@ -43187,6 +43257,27 @@ "react-native": ">=0.59.0" } }, + "node_modules/react-native-tab-view": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-4.0.2.tgz", + "integrity": "sha512-u70Et9onvAGhcBRda8NO+JgM/10G3kg/jWsbDY7QQSOAri7F7+VVS/4uupxjbCDSkL1BJdUOqhUToK/M2AH34Q==", + "dependencies": { + "use-latest-callback": "^0.2.1" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*", + "react-native-pager-view": ">= 6.0.0" + } + }, + "node_modules/react-native-tab-view/node_modules/use-latest-callback": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.3.tgz", + "integrity": "sha512-7vI3fBuyRcP91pazVboc4qu+6ZqM8izPWX9k7cRnT8hbD5svslcknsh3S9BUhaK11OmgTV4oWZZVSeQAiV53SQ==", + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/react-native-theme-switch-animation": { "version": "0.6.0", "license": "MIT", @@ -43507,6 +43598,7 @@ }, "node_modules/regenerator-transform": { "version": "0.15.1", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.4" @@ -43859,6 +43951,7 @@ }, "node_modules/serialize-javascript": { "version": "6.0.1", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" @@ -44516,6 +44609,7 @@ }, "node_modules/tapable": { "version": "2.2.1", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -44593,6 +44687,7 @@ "version": "5.3.10", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", "jest-worker": "^27.4.5", @@ -44626,6 +44721,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -44639,6 +44735,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -45215,6 +45312,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -45238,6 +45336,7 @@ "version": "5.94.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", @@ -45353,6 +45452,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, "engines": { "node": ">=10.13.0" } diff --git a/apps/mobile/scripts/optimize-fonts.mjs b/apps/mobile/scripts/optimize-fonts.mjs index b5027bd8e..bc0e0b0a6 100644 --- a/apps/mobile/scripts/optimize-fonts.mjs +++ b/apps/mobile/scripts/optimize-fonts.mjs @@ -83,7 +83,16 @@ const EXTRA_ICON_NAMES = [ "arrow-up-bold", "login", "gift", - "share" + "share", + "note-outline", + "weather-sunny", + "weather-night", + "notebook-outline", + "archive", + "home-outline", + "checkbox-blank-outline", + "checkbox-outline", + "checkbox-intermediate" ]; const __filename = fileURLToPath(import.meta.url); diff --git a/packages/editor-mobile/package-lock.json b/packages/editor-mobile/package-lock.json index 07534718d..eb6859d79 100644 --- a/packages/editor-mobile/package-lock.json +++ b/packages/editor-mobile/package-lock.json @@ -63,6 +63,7 @@ "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { + "@notesnook-importer/core": "^2.1.1", "@notesnook/common": "file:../common", "@notesnook/intl": "file:../intl", "@notesnook/theme": "file:../theme", diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index 6d13597ca..bdbd1df93 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2443,5 +2443,13 @@ Use this if changes from other devices are not appearing on this device. This wi fontLigatures: () => t`Font ligatures`, fontLigaturesDesc: () => t`Enable ligatures for common symbols like →, ←, etc`, - expandSidebar: () => t`Expand sidebar` + expandSidebar: () => t`Expand sidebar`, + viewAllLimits: () => `View all limits`, + freePlan: () => t`Free plan`, + proPlan: () => t`Pro plan`, + essentialPlan: () => t`Essential plan`, + believerPlan: () => t`Believer plan`, + storage: () => t`Storage`, + used: () => t`used`, + editProfile: () => t`Edit profile` }; diff --git a/packages/theme/package-lock.json b/packages/theme/package-lock.json index 7908c5df6..e7a33fe7c 100644 --- a/packages/theme/package-lock.json +++ b/packages/theme/package-lock.json @@ -417,9 +417,9 @@ } }, "node_modules/@theme-ui/css": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@theme-ui/css/-/css-0.16.1.tgz", - "integrity": "sha512-8TO2DbiqPrRyTlGRIElDak/p0M4ykyd8LkeavyOF/sTE9s93AwyFcle6KYYMEULrJP49SyYiEvTif7J7Z50DhA==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@theme-ui/css/-/css-0.16.2.tgz", + "integrity": "sha512-fNe+FhwKC5+7jQfxCRnm3oqYNhMFuiWiLA9OoLBEkt3b4egot29UK1+fqemwiNVjl206e2fPT5Z7uXRdb6LC2A==", "dev": true, "dependencies": { "csstype": "^3.0.10"