diff --git a/apps/mobile/app/app.tsx b/apps/mobile/app/app.tsx index 6cf5dd016..96b8a6fe7 100644 --- a/apps/mobile/app/app.tsx +++ b/apps/mobile/app/app.tsx @@ -23,7 +23,7 @@ import { THEME_COMPATIBILITY_VERSION, useThemeEngineStore } from "@notesnook/theme"; -import React, { PropsWithChildren, useEffect } from "react"; +import React, { PropsWithChildren, useEffect, useState } from "react"; import { Appearance, I18nManager, Linking, StatusBar } from "react-native"; import "react-native-gesture-handler"; import { GestureHandlerRootView } from "react-native-gesture-handler"; @@ -44,26 +44,37 @@ import { useUserStore } from "./stores/use-user-store"; import RNBootSplash from "react-native-bootsplash"; import AppLocked from "./components/app-lock"; import { useSettingStore } from "./stores/use-setting-store"; +import { + initShortcutListener, + launchNewNoteTab, + registerAppShortcuts +} from "./hooks/use-shortcut-manager"; +import Shortcuts from "react-native-actions-shortcuts"; I18nManager.allowRTL(false); I18nManager.forceRTL(false); I18nManager.swapLeftAndRightInRTL(false); + const { appLockEnabled, appLockMode } = SettingsService.get(); if (appLockEnabled || appLockMode !== "none") { useUserStore.getState().lockApp(true); } -RNBootSplash.hide({ - fade: true -}); -Linking.getInitialURL().then((url) => { - useSettingStore.setState({ - initialUrl: url - }); -}); + const App = (props: { configureMode: "note-preview" }) => { useAppEvents(); //@ts-ignore globalThis["IS_MAIN_APP_RUNNING"] = true; + const introCompleted = useSettingStore( + (state) => state.settings.introCompleted + ); + useEffect(() => { + if (introCompleted) { + registerAppShortcuts(); + } + }, [introCompleted]); + + useEffect(() => { + RNBootSplash.hide({ fade: true }); SettingsService.onFirstLaunch(); changeSystemBarColors(); SettingsService.setPrivacyScreen( @@ -176,4 +187,41 @@ export const withTheme = ( }; }; -export default withTheme(withErrorBoundry(App, "App")); +export const withStartupBoundry = ( + Element: (props: PropsWithChildren) => JSX.Element +) => { + return function AppWithStartupBoundary(props: PropsWithChildren) { + const [ready, setReady] = useState(false); + + useEffect(() => { + async function init() { + try { + const [url, shortcut] = await Promise.all([ + Linking.getInitialURL(), + Shortcuts.getInitialShortcut() + ]); + console.log(url, shortcut); + if (shortcut?.type === "notesnook.action.newnote") { + launchNewNoteTab(); + } + useSettingStore.setState({ + initialUrl: url, + pendingShortcut: shortcut ?? null + }); + + initShortcutListener(); + } finally { + setReady(true); + } + } + + init(); + }, []); + + if (!ready) return null; + + return ; + }; +}; + +export default withStartupBoundry(withTheme(withErrorBoundry(App, "App"))); diff --git a/apps/mobile/app/components/dialog/index.tsx b/apps/mobile/app/components/dialog/index.tsx index c4e6a1080..3eaa4d092 100644 --- a/apps/mobile/app/components/dialog/index.tsx +++ b/apps/mobile/app/components/dialog/index.tsx @@ -143,9 +143,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => { }, [hide, show]); const onNegativePress = async () => { - if (dialogInfo?.onClose) { - await dialogInfo.onClose(); - } hide(); }; diff --git a/apps/mobile/app/components/fluid-panels/index.tsx b/apps/mobile/app/components/fluid-panels/index.tsx index c2c0d0372..45913a669 100644 --- a/apps/mobile/app/components/fluid-panels/index.tsx +++ b/apps/mobile/app/components/fluid-panels/index.tsx @@ -51,9 +51,10 @@ interface TabProps extends ViewProps { onScroll: (offset: number) => void; enabled: boolean; onDrawerStateChange: (state: boolean) => void; + initialPage?: FluidTabPage; } -type FluidTabPage = "home" | "editor"; +export type FluidTabPage = "home" | "editor"; export interface TabsRef { goToPage: (page: FluidTabPage, animated?: boolean) => void; @@ -77,15 +78,21 @@ export const FluidPanels = forwardRef(function FluidTabs( onChangeTab, onScroll, enabled, - onDrawerStateChange + onDrawerStateChange, + initialPage }: TabProps, ref ) { const deviceMode = useSettingStore((state) => state.deviceMode); const fullscreen = useSettingStore((state) => state.fullscreen); - const translateX = useSharedValue(widths ? widths.sidebar : 0); + const editorStartPosition = widths.sidebar + widths.list; + const translateX = useSharedValue( + initialPage === "editor" ? editorStartPosition : widths ? widths.sidebar : 0 + ); const startX = useSharedValue(0); - const currentTab = useSharedValue(1); + const currentTab = useSharedValue( + initialPage === "editor" && deviceMode !== "tablet" ? 2 : 1 + ); const previousTab = useSharedValue(1); const isDrawerOpen = useSharedValue(false); const gestureStartValue = useSharedValue({ diff --git a/apps/mobile/app/hooks/use-shortcut-manager.ts b/apps/mobile/app/hooks/use-shortcut-manager.ts index cb3ff2fee..9b990560c 100644 --- a/apps/mobile/app/hooks/use-shortcut-manager.ts +++ b/apps/mobile/app/hooks/use-shortcut-manager.ts @@ -17,54 +17,67 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ import Shortcuts, { ShortcutItem } from "react-native-actions-shortcuts"; -import { useEffect } from "react"; -import { NativeEventEmitter, NativeModule } from "react-native"; -import { useRef } from "react"; -import { Platform } from "react-native"; +import { NativeEventEmitter, NativeModule, Platform } from "react-native"; import deviceInfoModule from "react-native-device-info"; import { strings } from "@notesnook/intl"; +import { useSettingStore } from "../stores/use-setting-store"; +import { useTabStore } from "../screens/editor/tiptap/use-tab-store"; + const ShortcutsEmitter = new NativeEventEmitter( Shortcuts as unknown as NativeModule ); -function isSupported() { +export function isShortcutsSupported() { return Platform.OS !== "android" || deviceInfoModule.getApiLevelSync() > 25; } + const defaultShortcuts: ShortcutItem[] = [ { type: "notesnook.action.newnote", title: strings.createNewNote(), shortTitle: strings.newNote(), iconName: Platform.OS === "android" ? "ic_newnote" : "plus" + }, + { + type: "notesnook.action.newreminder", + title: strings.setReminder(), + shortTitle: strings.newReminder(), + iconName: Platform.OS === "android" ? "ic_newnote" : "plus" } ]; -export const useShortcutManager = ({ - onShortcutPressed, - shortcuts = defaultShortcuts -}: { - onShortcutPressed: (shortcut: ShortcutItem | null) => void; - shortcuts?: ShortcutItem[]; -}) => { - const initialShortcutRecieved = useRef(false); - useEffect(() => { - if (!isSupported()) return; - Shortcuts.setShortcuts(shortcuts); - }, [shortcuts]); +export function registerAppShortcuts( + shortcuts: ShortcutItem[] = defaultShortcuts +) { + if (!isShortcutsSupported()) return; + Shortcuts.setShortcuts(shortcuts); +} - useEffect(() => { - if (!isSupported()) return; - Shortcuts.getInitialShortcut().then((shortcut) => { - if (initialShortcutRecieved.current || !shortcut) return; - onShortcutPressed(shortcut); - initialShortcutRecieved.current = true; - }); - const subscription = ShortcutsEmitter.addListener( - "onShortcutItemPressed", - onShortcutPressed - ); - return () => { - subscription?.remove(); - }; - }, [onShortcutPressed]); -}; +let listenerInitialized = false; +export function initShortcutListener() { + if (!isShortcutsSupported() || listenerInitialized) return; + listenerInitialized = true; + ShortcutsEmitter.addListener( + "onShortcutItemPressed", + (shortcut: ShortcutItem) => { + console.time("shortcut"); + useSettingStore.setState({ pendingShortcut: shortcut }); + } + ); +} + +export function launchNewNoteTab() { + let tabId; + const currentTab = useTabStore + .getState() + .getTab(useTabStore.getState().currentTab as string); + + if (useTabStore.getState().tabs.length === 0 || currentTab?.pinned) { + tabId = useTabStore.getState().newTab(); + } else { + tabId = useTabStore.getState().currentTab; + if (useTabStore.getState().getTab(tabId)?.session?.noteId) { + useTabStore.getState().newTabSession(tabId, {}); + } + } +} diff --git a/apps/mobile/app/navigation/fluid-panels-view.tsx b/apps/mobile/app/navigation/fluid-panels-view.tsx index 2ef17c040..e2650aef9 100644 --- a/apps/mobile/app/navigation/fluid-panels-view.tsx +++ b/apps/mobile/app/navigation/fluid-panels-view.tsx @@ -41,10 +41,9 @@ import Animated, { } from "react-native-reanimated"; import { notesnook } from "../../e2e/test.ids"; import { db } from "../common/database"; -import { FluidPanels } from "../components/fluid-panels"; +import { FluidPanels, FluidTabPage } from "../components/fluid-panels"; 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 { editorController, editorState } from "../screens/editor/tiptap/utils"; @@ -59,7 +58,6 @@ import { eCloseFullscreenEditor, eOnEnterEditor, eOnExitEditor, - eOnLoadNote, eOpenFullscreenEditor, eUnlockNote } from "../utils/events"; @@ -67,6 +65,7 @@ import { valueLimiter } from "../utils/functions"; import { fluidTabsRef } from "../utils/global-refs"; import { AppNavigationStack } from "./navigation-stack"; import type { PaneWidths } from "../screens/editor/wrapper"; +import { NavigationProps } from "../services/navigation"; const MOBILE_SIDEBAR_SIZE = 0.85; @@ -74,7 +73,7 @@ let SideMenu: any = null; let EditorWrapper: any = null; export const FluidPanelsView = React.memo( - () => { + ({ route }: NavigationProps<"FluidPanelsView">) => { const { colors } = useThemeColors(); const deviceMode = useSettingStore((state) => state.deviceMode); const setFullscreen = useSettingStore((state) => state.setFullscreen); @@ -102,6 +101,15 @@ export const FluidPanelsView = React.memo( setOrientation(o); } }); + React.useEffect(() => { + const shortcut = useSettingStore.getState().pendingShortcut; + + if (shortcut?.type === "notesnook.action.newnote") { + useSettingStore.setState({ + pendingShortcut: null + }); + } + }, []); useEffect(() => { if (!appLoading) { @@ -111,29 +119,6 @@ export const FluidPanelsView = React.memo( } }, [appLoading]); - useShortcutManager({ - onShortcutPressed: async (item) => { - if (!item) return; - - if (item?.type === "notesnook.action.newnote") { - if (!fluidTabsRef.current) { - setTimeout(() => { - eSendEvent(eOnLoadNote, { newNote: true }); - editorState().movedAway = false; - fluidTabsRef.current?.goToPage("editor", false); - }, 1000); - return; - } - eSendEvent(eOnLoadNote, { newNote: true }); - editorState().movedAway = false; - setTimeout( - () => fluidTabsRef.current?.goToPage("editor", false), - 300 - ); - } - } - }); - const showFullScreenEditor = useCallback(() => { setFullscreen(true); if (deviceMode === "smallTablet") { @@ -356,6 +341,7 @@ export const FluidPanelsView = React.memo( dimensions={dimensions} widths={PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS]} enabled={deviceMode !== "tablet" && !fullscreen} + initialPage={route.params?.initialPage} onScroll={onScroll} onChangeTab={onChangeTab} onDrawerStateChange={(state) => { diff --git a/apps/mobile/app/navigation/navigation-stack.tsx b/apps/mobile/app/navigation/navigation-stack.tsx index 570b7e1c8..486b4150c 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -27,10 +27,17 @@ 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"; +import { fluidTabsRef, rootNavigatorRef } from "../utils/global-refs"; import Navigation from "../services/navigation"; -import { isFeatureAvailable } from "@notesnook/common"; +import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common"; import { isInternalLink, parseInternalLink } from "@notesnook/core"; +import { eSendEvent } from "../services/event-manager"; +import { editorState } from "../screens/editor/tiptap/utils"; +import { eOnLoadNote } from "../utils/events"; +import { strings } from "@notesnook/intl"; +import PaywallSheet from "../components/sheets/paywall"; +import { presentDialog } from "../components/dialog/functions"; +import { launchNewNoteTab } from "../hooks/use-shortcut-manager"; const RootStack = createNativeStackNavigator(); const AppStack = createNativeStackNavigator(); @@ -300,8 +307,15 @@ export const RootNavigation = () => { const introCompleted = useSettingStore( (state) => state.settings.introCompleted ); + + const initialShortcut = React.useRef( + useSettingStore.getState().pendingShortcut + ).current; + + const reminderFeature = useIsFeatureAvailable("activeReminders"); const clearSelection = useSelectionStore((state) => state.clearSelection); const resetTimer = React.useRef(undefined); + const onStateChange = React.useCallback( (state: any) => { if (useSelectionStore.getState().selectionMode) { @@ -316,13 +330,66 @@ export const RootNavigation = () => { [clearSelection] ); + React.useEffect(() => { + const unsubscribe = useSettingStore.subscribe((state, prevState) => { + const pendingShortcut = state.pendingShortcut; + + if (pendingShortcut === prevState.pendingShortcut || !pendingShortcut) { + return; + } + + if (pendingShortcut.type === "notesnook.action.newreminder") { + if (reminderFeature === undefined) return; + + if (!reminderFeature.isAllowed) { + presentDialog({ + title: strings.upgrade(), + paragraph: reminderFeature.error, + positiveText: strings.upgrade(), + negativeText: strings.cancel(), + positivePress: async () => { + PaywallSheet.present(reminderFeature); + } + }); + useSettingStore.setState({ + pendingShortcut: null + }); + return; + } + + rootNavigatorRef.current?.navigate("AddReminder" as any); + } else if (pendingShortcut.type === "notesnook.action.newnote") { + if (fluidTabsRef.current) { + rootNavigatorRef.current?.navigate("FluidPanelsView" as any); + eSendEvent(eOnLoadNote, { newNote: true }); + editorState().movedAway = false; + fluidTabsRef.current.goToPage("editor", true); + } else { + launchNewNoteTab(); + + rootNavigatorRef.current?.navigate("FluidPanelsView" as any, { + initialPage: "editor" + }); + } + } + }); + + return unsubscribe; + }, [reminderFeature]); + + const initialRouteName = !introCompleted + ? "Welcome" + : initialShortcut?.type === "notesnook.action.newreminder" + ? "AddReminder" + : "FluidPanelsView"; + return ( { require("../navigation/fluid-panels-view").default; return FluidPanelsView; }} + initialParams={{ + initialPage: + initialShortcut?.type === "notesnook.action.newnote" + ? "editor" + : undefined + }} /> ) { - const { reminder, reference } = props.route.params; + const { reminder, reference } = props.route.params ?? {}; useNavigationFocus(props.navigation, { focusOnInit: true, onFocus: () => { @@ -106,6 +108,23 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { return false; } }); + const handleBackNavigation = useCallback(() => { + const routes = props.navigation.getState()?.routes; + if (routes && routes.length <= 1) { + props.navigation.navigate("FluidPanelsView" as any); + return true; + } + Navigation.goBack(); + return true; + }, [props.navigation]); + + useEffect(() => { + const sub = BackHandler.addEventListener("hardwareBackPress", () => { + return handleBackNavigation(); + }); + return () => sub.remove(); + }, [handleBackNavigation]); + const { colors, isDark } = useThemeColors(); const weekFormat = useSettingStore((state) => state.weekFormat); const [reminderMode, setReminderMode] = useState( @@ -127,6 +146,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { const [repeatFrequency, setRepeatFrequency] = useState(1); const referencedItem = reference ? (reference as Note) : null; const recurringReminderFeature = useIsFeatureAvailable("recurringReminders"); + const activeReminderFeature = useIsFeatureAvailable("activeReminders"); const formRef = useRef( createFormRef({ title: reminder?.title || referencedItem?.title || "", @@ -153,6 +173,31 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { ); const [dateError, setDateError] = useState(); const [selectDayError, setSelectDayError] = useState(); + React.useEffect(() => { + const shortcut = useSettingStore.getState().pendingShortcut; + if (shortcut?.type === "notesnook.action.newreminder") { + useSettingStore.setState({ + pendingShortcut: null + }); + } + }, []); + useEffect(() => { + if (activeReminderFeature === undefined) return; + if (!activeReminderFeature.isAllowed) { + presentDialog({ + title: strings.upgrade(), + paragraph: activeReminderFeature.error, + positiveText: strings.upgrade(), + negativeText: strings.cancel(), + positivePress: async () => { + PaywallSheet.present(activeReminderFeature); + }, + onClose: () => { + props.navigation.navigate("FluidPanelsView" as any); + } + }); + } + }, [activeReminderFeature]); const showDatePicker = () => { setDatePickerVisibility(true); @@ -243,7 +288,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { Notifications.scheduleNotification(_reminder as Reminder); Navigation.queueRoutesForUpdate(); useRelationStore.getState().update(); - Navigation.goBack(); + handleBackNavigation(); } catch (e) { ToastManager.error(e as Error, undefined); } @@ -267,12 +312,12 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
- - (route.name !== "Auth" && route.name !== "Welcome") || + (route.name !== "Auth" && + route.name !== "Welcome" && + route.name !== "AddReminder") || route.key === focusedRoute.key ); diff --git a/apps/mobile/app/stores/use-navigation-store.ts b/apps/mobile/app/stores/use-navigation-store.ts index 2d98a26ff..7adfbdc67 100644 --- a/apps/mobile/app/stores/use-navigation-store.ts +++ b/apps/mobile/app/stores/use-navigation-store.ts @@ -96,7 +96,7 @@ export interface RouteParams extends ParamListBase { Monographs: NotesScreenParams; Reminders: GenericRouteParam; SettingsGroup: GenericRouteParam; - FluidPanelsView: GenericRouteParam; + FluidPanelsView: { initialPage?: "editor" | "home" }; AppLock: GenericRouteParam; Settings: GenericRouteParam; Auth: AuthParams; diff --git a/apps/mobile/app/stores/use-setting-store.ts b/apps/mobile/app/stores/use-setting-store.ts index 6fc0f67f7..ea5aa5c28 100644 --- a/apps/mobile/app/stores/use-setting-store.ts +++ b/apps/mobile/app/stores/use-setting-store.ts @@ -27,6 +27,7 @@ import { ThemeDark, ThemeLight, ThemeDefinition } from "@notesnook/theme"; import { DayFormat, WeekFormat, Reminder } from "@notesnook/core"; import { db } from "../common/database"; import { EDITOR_LINE_HEIGHT } from "../utils/constants"; +import { ShortcutItem } from "react-native-actions-shortcuts"; export const HostIds = [ "API_HOST", "AUTH_HOST", @@ -149,6 +150,7 @@ export interface SettingStore { refresh: () => void; inboxEnabled: boolean; setInboxEnabled: (inboxEnabled: boolean) => void; + pendingShortcut: ShortcutItem | null; } const { width, height } = Dimensions.get("window"); @@ -269,5 +271,6 @@ export const useSettingStore = create((set, get) => ({ }); }, inboxEnabled: false, - setInboxEnabled: (inboxEnabled) => set({ inboxEnabled }) + setInboxEnabled: (inboxEnabled) => set({ inboxEnabled }), + pendingShortcut: null }));