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
}));