From 64943647f81584e53b2e641688153eb5bcd98c5b Mon Sep 17 00:00:00 2001 From: kashaf-ansari-dev Date: Thu, 25 Jun 2026 22:10:11 +0500 Subject: [PATCH 01/39] mobile: add app shortcut for creating reminders Signed-off-by: kashaf-ansari-dev --- apps/mobile/app/hooks/use-shortcut-manager.ts | 6 ++++++ apps/mobile/app/navigation/fluid-panels-view.tsx | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/apps/mobile/app/hooks/use-shortcut-manager.ts b/apps/mobile/app/hooks/use-shortcut-manager.ts index cb3ff2fee..011c463a8 100644 --- a/apps/mobile/app/hooks/use-shortcut-manager.ts +++ b/apps/mobile/app/hooks/use-shortcut-manager.ts @@ -36,6 +36,12 @@ const defaultShortcuts: ShortcutItem[] = [ 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 = ({ diff --git a/apps/mobile/app/navigation/fluid-panels-view.tsx b/apps/mobile/app/navigation/fluid-panels-view.tsx index 2ef17c040..9cb659e8a 100644 --- a/apps/mobile/app/navigation/fluid-panels-view.tsx +++ b/apps/mobile/app/navigation/fluid-panels-view.tsx @@ -67,6 +67,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 AddReminder from "../screens/add-reminder"; const MOBILE_SIDEBAR_SIZE = 0.85; @@ -131,6 +132,11 @@ export const FluidPanelsView = React.memo( 300 ); } + if (item?.type === "notesnook.action.newreminder") { + setTimeout(() => { + AddReminder.present(); + }, 1000); + } } }); From 180c202c4e613d24a71fdde2ffddb03d183d510d Mon Sep 17 00:00:00 2001 From: kashaf-ansari-dev Date: Tue, 7 Jul 2026 14:28:23 +0500 Subject: [PATCH 02/39] mobile: handle app shortcuts correctly during cold and warm app launches Signed-off-by: kashaf-ansari-dev --- apps/mobile/app/app.tsx | 19 +++++++ apps/mobile/app/hooks/use-shortcut-manager.ts | 21 +++----- .../app/navigation/fluid-panels-view.tsx | 52 +++++++++++-------- .../app/navigation/navigation-stack.tsx | 17 +++++- .../mobile/app/screens/add-reminder/index.tsx | 2 +- 5 files changed, 71 insertions(+), 40 deletions(-) diff --git a/apps/mobile/app/app.tsx b/apps/mobile/app/app.tsx index 6cf5dd016..6224cf8e6 100644 --- a/apps/mobile/app/app.tsx +++ b/apps/mobile/app/app.tsx @@ -44,9 +44,15 @@ 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 { registerAppShortcuts } from "./hooks/use-shortcut-manager"; +import Shortcuts, { ShortcutItem } from "react-native-actions-shortcuts"; I18nManager.allowRTL(false); I18nManager.forceRTL(false); I18nManager.swapLeftAndRightInRTL(false); +declare global { + var __pendingShortcut: ShortcutItem | null | undefined; +} + const { appLockEnabled, appLockMode } = SettingsService.get(); if (appLockEnabled || appLockMode !== "none") { useUserStore.getState().lockApp(true); @@ -59,10 +65,23 @@ Linking.getInitialURL().then((url) => { initialUrl: url }); }); +Shortcuts.getInitialShortcut().then((shortcut) => { + globalThis.__pendingShortcut = shortcut; +}); 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(() => { SettingsService.onFirstLaunch(); changeSystemBarColors(); diff --git a/apps/mobile/app/hooks/use-shortcut-manager.ts b/apps/mobile/app/hooks/use-shortcut-manager.ts index 011c463a8..7f9fcb614 100644 --- a/apps/mobile/app/hooks/use-shortcut-manager.ts +++ b/apps/mobile/app/hooks/use-shortcut-manager.ts @@ -45,26 +45,12 @@ const defaultShortcuts: ShortcutItem[] = [ } ]; export const useShortcutManager = ({ - onShortcutPressed, - shortcuts = defaultShortcuts + onShortcutPressed }: { onShortcutPressed: (shortcut: ShortcutItem | null) => void; - shortcuts?: ShortcutItem[]; }) => { - const initialShortcutRecieved = useRef(false); - useEffect(() => { if (!isSupported()) return; - Shortcuts.setShortcuts(shortcuts); - }, [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 @@ -74,3 +60,8 @@ export const useShortcutManager = ({ }; }, [onShortcutPressed]); }; + +export const registerAppShortcuts = () => { + if (!isSupported()) return; + Shortcuts.setShortcuts(defaultShortcuts); +}; diff --git a/apps/mobile/app/navigation/fluid-panels-view.tsx b/apps/mobile/app/navigation/fluid-panels-view.tsx index 9cb659e8a..7a9a601d5 100644 --- a/apps/mobile/app/navigation/fluid-panels-view.tsx +++ b/apps/mobile/app/navigation/fluid-panels-view.tsx @@ -68,6 +68,8 @@ import { fluidTabsRef } from "../utils/global-refs"; import { AppNavigationStack } from "./navigation-stack"; import type { PaneWidths } from "../screens/editor/wrapper"; import AddReminder from "../screens/add-reminder"; +import Navigation from "../services/navigation"; +import { ShortcutItem } from "react-native-actions-shortcuts"; const MOBILE_SIDEBAR_SIZE = 0.85; @@ -112,33 +114,37 @@ export const FluidPanelsView = React.memo( } }, [appLoading]); - useShortcutManager({ - onShortcutPressed: async (item) => { - if (!item) return; + useEffect(() => { + const pending = globalThis.__pendingShortcut; + if ( + pending?.type === "notesnook.action.newnote" && + fluidTabsRef.current && + !appLoading + ) { + eSendEvent(eOnLoadNote, { newNote: true }); + editorState().movedAway = false; + fluidTabsRef.current.goToPage("editor", false); + globalThis.__pendingShortcut = null; + } + }, [deviceMode, appLoading]); - if (item?.type === "notesnook.action.newnote") { - if (!fluidTabsRef.current) { - setTimeout(() => { - eSendEvent(eOnLoadNote, { newNote: true }); - editorState().movedAway = false; - fluidTabsRef.current?.goToPage("editor", false); - }, 1000); - return; - } + const onShortcutPressed = useCallback(async (item: ShortcutItem | null) => { + if (!item) return; + + if (item?.type === "notesnook.action.newnote") { + Navigation.navigate("FluidPanelsView"); + requestAnimationFrame(() => { eSendEvent(eOnLoadNote, { newNote: true }); editorState().movedAway = false; - setTimeout( - () => fluidTabsRef.current?.goToPage("editor", false), - 300 - ); - } - if (item?.type === "notesnook.action.newreminder") { - setTimeout(() => { - AddReminder.present(); - }, 1000); - } + fluidTabsRef.current?.goToPage("editor", false); + }); } - }); + if (item?.type === "notesnook.action.newreminder") { + AddReminder.present(); + } + }, []); + + useShortcutManager({ onShortcutPressed }); const showFullScreenEditor = useCallback(() => { setFullscreen(true); diff --git a/apps/mobile/app/navigation/navigation-stack.tsx b/apps/mobile/app/navigation/navigation-stack.tsx index 570b7e1c8..6dd0e4121 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -316,8 +316,23 @@ export const RootNavigation = () => { [clearSelection] ); + const onNavigationReady = React.useCallback(() => { + const pending = globalThis.__pendingShortcut; + if (pending?.type === "notesnook.action.newreminder") { + rootNavigatorRef.current?.navigate("AddReminder", { + reminder: undefined, + reference: undefined + }); + globalThis.__pendingShortcut = null; + } + }, []); + return ( - + ) { - const { reminder, reference } = props.route.params; + const { reminder, reference } = props.route.params ?? {}; useNavigationFocus(props.navigation, { focusOnInit: true, onFocus: () => { From a40814a3ab575db4ab6934480d95cdf2d6c2b8c6 Mon Sep 17 00:00:00 2001 From: kashaf-ansari-dev Date: Sat, 11 Jul 2026 19:14:30 +0500 Subject: [PATCH 03/39] mobile: revise app shortcut handling implementation Signed-off-by: kashaf-ansari-dev --- apps/mobile/app/app.tsx | 26 +++---- .../app/components/fluid-panels/index.tsx | 13 ++-- apps/mobile/app/hooks/use-shortcut-manager.ts | 48 +++++++------ .../app/navigation/fluid-panels-view.tsx | 50 ++++---------- .../app/navigation/navigation-stack.tsx | 69 +++++++++++++++---- .../mobile/app/screens/add-reminder/index.tsx | 21 +++++- apps/mobile/app/stores/use-setting-store.ts | 7 +- 7 files changed, 140 insertions(+), 94 deletions(-) diff --git a/apps/mobile/app/app.tsx b/apps/mobile/app/app.tsx index 6224cf8e6..b8fd22ba3 100644 --- a/apps/mobile/app/app.tsx +++ b/apps/mobile/app/app.tsx @@ -44,30 +44,32 @@ 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 { registerAppShortcuts } from "./hooks/use-shortcut-manager"; -import Shortcuts, { ShortcutItem } from "react-native-actions-shortcuts"; +import { + initShortcutListener, + registerAppShortcuts +} from "./hooks/use-shortcut-manager"; +import Shortcuts from "react-native-actions-shortcuts"; I18nManager.allowRTL(false); I18nManager.forceRTL(false); I18nManager.swapLeftAndRightInRTL(false); -declare global { - var __pendingShortcut: ShortcutItem | null | undefined; -} const { appLockEnabled, appLockMode } = SettingsService.get(); if (appLockEnabled || appLockMode !== "none") { useUserStore.getState().lockApp(true); } -RNBootSplash.hide({ - fade: true -}); +initShortcutListener(); Linking.getInitialURL().then((url) => { - useSettingStore.setState({ - initialUrl: url - }); + useSettingStore.setState({ initialUrl: url }); }); + Shortcuts.getInitialShortcut().then((shortcut) => { - globalThis.__pendingShortcut = shortcut; + useSettingStore.setState({ + pendingShortcut: shortcut ?? null, + pendingShortcutLoaded: true + }); + RNBootSplash.hide({ fade: true }); }); + const App = (props: { configureMode: "note-preview" }) => { useAppEvents(); //@ts-ignore diff --git a/apps/mobile/app/components/fluid-panels/index.tsx b/apps/mobile/app/components/fluid-panels/index.tsx index c2c0d0372..072be6f7e 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,19 @@ 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" ? 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 7f9fcb614..224ac9a09 100644 --- a/apps/mobile/app/hooks/use-shortcut-manager.ts +++ b/apps/mobile/app/hooks/use-shortcut-manager.ts @@ -17,19 +17,19 @@ 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"; + 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", @@ -44,24 +44,22 @@ const defaultShortcuts: ShortcutItem[] = [ iconName: Platform.OS === "android" ? "ic_newnote" : "plus" } ]; -export const useShortcutManager = ({ - onShortcutPressed -}: { - onShortcutPressed: (shortcut: ShortcutItem | null) => void; -}) => { - useEffect(() => { - if (!isSupported()) return; - const subscription = ShortcutsEmitter.addListener( - "onShortcutItemPressed", - onShortcutPressed - ); - return () => { - subscription?.remove(); - }; - }, [onShortcutPressed]); -}; -export const registerAppShortcuts = () => { - if (!isSupported()) return; - Shortcuts.setShortcuts(defaultShortcuts); -}; +export function registerAppShortcuts( + shortcuts: ShortcutItem[] = defaultShortcuts +) { + if (!isShortcutsSupported()) return; + Shortcuts.setShortcuts(shortcuts); +} + +let listenerInitialized = false; +export function initShortcutListener() { + if (!isShortcutsSupported() || listenerInitialized) return; + listenerInitialized = true; + ShortcutsEmitter.addListener( + "onShortcutItemPressed", + (shortcut: ShortcutItem) => { + useSettingStore.setState({ pendingShortcut: shortcut }); + } + ); +} diff --git a/apps/mobile/app/navigation/fluid-panels-view.tsx b/apps/mobile/app/navigation/fluid-panels-view.tsx index 7a9a601d5..677f6ac31 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"; @@ -67,9 +66,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 AddReminder from "../screens/add-reminder"; import Navigation from "../services/navigation"; -import { ShortcutItem } from "react-native-actions-shortcuts"; const MOBILE_SIDEBAR_SIZE = 0.85; @@ -94,6 +91,18 @@ export const FluidPanelsView = React.memo( ); const appLoading = useSettingStore((state) => state.isAppLoading); const [isLoading, setIsLoading] = useState(false); + const pendingShortcut = useSettingStore((state) => state.pendingShortcut); + const [initialPane] = useState(() => + pendingShortcut?.type === "notesnook.action.newnote" ? "editor" : "home" + ); + + useEffect(() => { + if (pendingShortcut?.type === "notesnook.action.newnote") { + eSendEvent(eOnLoadNote, { newNote: true }); + editorState().movedAway = false; + useSettingStore.setState({ pendingShortcut: null }); + } + }, []); useDeviceOrientationChange((o) => { if ( @@ -114,38 +123,6 @@ export const FluidPanelsView = React.memo( } }, [appLoading]); - useEffect(() => { - const pending = globalThis.__pendingShortcut; - if ( - pending?.type === "notesnook.action.newnote" && - fluidTabsRef.current && - !appLoading - ) { - eSendEvent(eOnLoadNote, { newNote: true }); - editorState().movedAway = false; - fluidTabsRef.current.goToPage("editor", false); - globalThis.__pendingShortcut = null; - } - }, [deviceMode, appLoading]); - - const onShortcutPressed = useCallback(async (item: ShortcutItem | null) => { - if (!item) return; - - if (item?.type === "notesnook.action.newnote") { - Navigation.navigate("FluidPanelsView"); - requestAnimationFrame(() => { - eSendEvent(eOnLoadNote, { newNote: true }); - editorState().movedAway = false; - fluidTabsRef.current?.goToPage("editor", false); - }); - } - if (item?.type === "notesnook.action.newreminder") { - AddReminder.present(); - } - }, []); - - useShortcutManager({ onShortcutPressed }); - const showFullScreenEditor = useCallback(() => { setFullscreen(true); if (deviceMode === "smallTablet") { @@ -368,6 +345,7 @@ export const FluidPanelsView = React.memo( dimensions={dimensions} widths={PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS]} enabled={deviceMode !== "tablet" && !fullscreen} + initialPage={initialPane} 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 6dd0e4121..7dc5cac78 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -27,10 +27,13 @@ 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 { isInternalLink, parseInternalLink } from "@notesnook/core"; +import { eSendEvent } from "../services/event-manager"; +import { editorState } from "../screens/editor/tiptap/utils"; +import { eOnLoadNote } from "../utils/events"; const RootStack = createNativeStackNavigator(); const AppStack = createNativeStackNavigator(); @@ -300,8 +303,13 @@ export const RootNavigation = () => { const introCompleted = useSettingStore( (state) => state.settings.introCompleted ); + const pendingShortcut = useSettingStore((state) => state.pendingShortcut); + const pendingShortcutLoaded = useSettingStore( + (state) => state.pendingShortcutLoaded + ); const clearSelection = useSelectionStore((state) => state.clearSelection); const resetTimer = React.useRef(undefined); + const isFirstRun = React.useRef(true); const onStateChange = React.useCallback( (state: any) => { if (useSelectionStore.getState().selectionMode) { @@ -316,28 +324,59 @@ export const RootNavigation = () => { [clearSelection] ); - const onNavigationReady = React.useCallback(() => { - const pending = globalThis.__pendingShortcut; - if (pending?.type === "notesnook.action.newreminder") { - rootNavigatorRef.current?.navigate("AddReminder", { - reminder: undefined, - reference: undefined - }); - globalThis.__pendingShortcut = null; + React.useEffect(() => { + if (pendingShortcut?.type === "notesnook.action.newreminder") { + useSettingStore.setState({ pendingShortcut: null }); } }, []); + React.useEffect(() => { + if (isFirstRun.current) { + isFirstRun.current = false; + return; + } + if (!pendingShortcut) return; + + const routes = rootNavigatorRef.current?.getState()?.routes; + const currentRoute = routes?.[routes.length - 1]?.name; + + if (pendingShortcut.type === "notesnook.action.newreminder") { + if (currentRoute !== "AddReminder") { + rootNavigatorRef.current?.navigate("AddReminder" as any); + } + useSettingStore.setState({ pendingShortcut: null }); + } else if (pendingShortcut.type === "notesnook.action.newnote") { + if (fluidTabsRef.current) { + if (currentRoute !== "FluidPanelsView") { + rootNavigatorRef.current?.navigate("FluidPanelsView" as any); + } + eSendEvent(eOnLoadNote, { newNote: true }); + editorState().movedAway = false; + fluidTabsRef.current.goToPage("editor", true); + useSettingStore.setState({ pendingShortcut: null }); + } else { + if (currentRoute !== "FluidPanelsView") { + rootNavigatorRef.current?.navigate("FluidPanelsView" as any); + } + } + } + }, [pendingShortcut]); + + if (!pendingShortcutLoaded) return null; + + const initialRouteName = !introCompleted + ? "Welcome" + : pendingShortcut?.type === "notesnook.action.newreminder" + ? "AddReminder" + : "FluidPanelsView"; + return ( - + ) { 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( @@ -267,6 +285,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
void; inboxEnabled: boolean; setInboxEnabled: (inboxEnabled: boolean) => void; + pendingShortcut: ShortcutItem | null; + pendingShortcutLoaded: boolean; } const { width, height } = Dimensions.get("window"); @@ -269,5 +272,7 @@ export const useSettingStore = create((set, get) => ({ }); }, inboxEnabled: false, - setInboxEnabled: (inboxEnabled) => set({ inboxEnabled }) + setInboxEnabled: (inboxEnabled) => set({ inboxEnabled }), + pendingShortcut: null, + pendingShortcutLoaded: false })); From c3c9b1fbf4634873bc8d96e2982ef1ad3e516b28 Mon Sep 17 00:00:00 2001 From: kashaf-ansari-dev Date: Tue, 14 Jul 2026 09:18:56 +0500 Subject: [PATCH 04/39] mobile: fix app shortcut navigation, flash, and back-nav issues Signed-off-by: kashaf-ansari-dev --- apps/mobile/app/app.tsx | 9 ++++--- .../app/components/fluid-panels/index.tsx | 4 ++- .../app/navigation/fluid-panels-view.tsx | 3 ++- .../app/navigation/navigation-stack.tsx | 27 ++++--------------- 4 files changed, 15 insertions(+), 28 deletions(-) diff --git a/apps/mobile/app/app.tsx b/apps/mobile/app/app.tsx index b8fd22ba3..e549ae93a 100644 --- a/apps/mobile/app/app.tsx +++ b/apps/mobile/app/app.tsx @@ -57,10 +57,6 @@ const { appLockEnabled, appLockMode } = SettingsService.get(); if (appLockEnabled || appLockMode !== "none") { useUserStore.getState().lockApp(true); } -initShortcutListener(); -Linking.getInitialURL().then((url) => { - useSettingStore.setState({ initialUrl: url }); -}); Shortcuts.getInitialShortcut().then((shortcut) => { useSettingStore.setState({ @@ -68,6 +64,11 @@ Shortcuts.getInitialShortcut().then((shortcut) => { pendingShortcutLoaded: true }); RNBootSplash.hide({ fade: true }); + initShortcutListener(); +}); + +Linking.getInitialURL().then((url) => { + useSettingStore.setState({ initialUrl: url }); }); const App = (props: { configureMode: "note-preview" }) => { diff --git a/apps/mobile/app/components/fluid-panels/index.tsx b/apps/mobile/app/components/fluid-panels/index.tsx index 072be6f7e..45913a669 100644 --- a/apps/mobile/app/components/fluid-panels/index.tsx +++ b/apps/mobile/app/components/fluid-panels/index.tsx @@ -90,7 +90,9 @@ export const FluidPanels = forwardRef(function FluidTabs( initialPage === "editor" ? editorStartPosition : widths ? widths.sidebar : 0 ); const startX = useSharedValue(0); - const currentTab = useSharedValue(initialPage === "editor" ? 2 : 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/navigation/fluid-panels-view.tsx b/apps/mobile/app/navigation/fluid-panels-view.tsx index 677f6ac31..0a5472555 100644 --- a/apps/mobile/app/navigation/fluid-panels-view.tsx +++ b/apps/mobile/app/navigation/fluid-panels-view.tsx @@ -100,9 +100,10 @@ export const FluidPanelsView = React.memo( if (pendingShortcut?.type === "notesnook.action.newnote") { eSendEvent(eOnLoadNote, { newNote: true }); editorState().movedAway = false; + fluidTabsRef.current?.goToPage("editor", true); useSettingStore.setState({ pendingShortcut: null }); } - }, []); + }, [pendingShortcut]); useDeviceOrientationChange((o) => { if ( diff --git a/apps/mobile/app/navigation/navigation-stack.tsx b/apps/mobile/app/navigation/navigation-stack.tsx index 7dc5cac78..6639c8a10 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -309,7 +309,7 @@ export const RootNavigation = () => { ); const clearSelection = useSelectionStore((state) => state.clearSelection); const resetTimer = React.useRef(undefined); - const isFirstRun = React.useRef(true); + const isNavigationLoaded = React.useRef(true); const onStateChange = React.useCallback( (state: any) => { if (useSelectionStore.getState().selectionMode) { @@ -331,34 +331,17 @@ export const RootNavigation = () => { }, []); React.useEffect(() => { - if (isFirstRun.current) { - isFirstRun.current = false; + if (isNavigationLoaded.current) { + isNavigationLoaded.current = false; return; } if (!pendingShortcut) return; - const routes = rootNavigatorRef.current?.getState()?.routes; - const currentRoute = routes?.[routes.length - 1]?.name; - if (pendingShortcut.type === "notesnook.action.newreminder") { - if (currentRoute !== "AddReminder") { - rootNavigatorRef.current?.navigate("AddReminder" as any); - } + rootNavigatorRef.current?.navigate("AddReminder" as any); useSettingStore.setState({ pendingShortcut: null }); } else if (pendingShortcut.type === "notesnook.action.newnote") { - if (fluidTabsRef.current) { - if (currentRoute !== "FluidPanelsView") { - rootNavigatorRef.current?.navigate("FluidPanelsView" as any); - } - eSendEvent(eOnLoadNote, { newNote: true }); - editorState().movedAway = false; - fluidTabsRef.current.goToPage("editor", true); - useSettingStore.setState({ pendingShortcut: null }); - } else { - if (currentRoute !== "FluidPanelsView") { - rootNavigatorRef.current?.navigate("FluidPanelsView" as any); - } - } + rootNavigatorRef.current?.navigate("FluidPanelsView" as any); } }, [pendingShortcut]); From 3e2847f84478ffd06a2e250a17d4dbb3c0a5973a Mon Sep 17 00:00:00 2001 From: kashaf-ansari-dev Date: Tue, 14 Jul 2026 19:06:56 +0500 Subject: [PATCH 05/39] mobile: add fluid panels initialRoute to simplified navigation Signed-off-by: kashaf-ansari-dev --- .../app/navigation/fluid-panels-view.tsx | 20 +++---------------- .../app/navigation/navigation-stack.tsx | 17 +++++++++++++++- .../mobile/app/stores/use-navigation-store.ts | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/apps/mobile/app/navigation/fluid-panels-view.tsx b/apps/mobile/app/navigation/fluid-panels-view.tsx index 0a5472555..1292c6452 100644 --- a/apps/mobile/app/navigation/fluid-panels-view.tsx +++ b/apps/mobile/app/navigation/fluid-panels-view.tsx @@ -58,7 +58,6 @@ import { eCloseFullscreenEditor, eOnEnterEditor, eOnExitEditor, - eOnLoadNote, eOpenFullscreenEditor, eUnlockNote } from "../utils/events"; @@ -66,7 +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 Navigation from "../services/navigation"; +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); @@ -91,19 +90,6 @@ export const FluidPanelsView = React.memo( ); const appLoading = useSettingStore((state) => state.isAppLoading); const [isLoading, setIsLoading] = useState(false); - const pendingShortcut = useSettingStore((state) => state.pendingShortcut); - const [initialPane] = useState(() => - pendingShortcut?.type === "notesnook.action.newnote" ? "editor" : "home" - ); - - useEffect(() => { - if (pendingShortcut?.type === "notesnook.action.newnote") { - eSendEvent(eOnLoadNote, { newNote: true }); - editorState().movedAway = false; - fluidTabsRef.current?.goToPage("editor", true); - useSettingStore.setState({ pendingShortcut: null }); - } - }, [pendingShortcut]); useDeviceOrientationChange((o) => { if ( @@ -346,7 +332,7 @@ export const FluidPanelsView = React.memo( dimensions={dimensions} widths={PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS]} enabled={deviceMode !== "tablet" && !fullscreen} - initialPage={initialPane} + 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 6639c8a10..486bedf14 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -341,7 +341,16 @@ export const RootNavigation = () => { rootNavigatorRef.current?.navigate("AddReminder" as any); useSettingStore.setState({ pendingShortcut: null }); } else if (pendingShortcut.type === "notesnook.action.newnote") { - rootNavigatorRef.current?.navigate("FluidPanelsView" as any); + rootNavigatorRef.current?.navigate("FluidPanelsView" as any, { + initialPage: !fluidTabsRef.current ? "editor" : undefined + }); + + if (fluidTabsRef.current) { + eSendEvent(eOnLoadNote, { newNote: true }); + editorState().movedAway = false; + fluidTabsRef.current.goToPage("editor", true); + useSettingStore.setState({ pendingShortcut: null }); + } } }, [pendingShortcut]); @@ -384,6 +393,12 @@ export const RootNavigation = () => { require("../navigation/fluid-panels-view").default; return FluidPanelsView; }} + initialParams={{ + initialPage: + pendingShortcut?.type === "notesnook.action.newnote" + ? "editor" + : undefined + }} /> Date: Thu, 16 Jul 2026 13:27:58 +0500 Subject: [PATCH 06/39] mobile: resolve app shortcut navigation and add reminder feature gate Signed-off-by: kashaf-ansari-dev --- apps/mobile/app/components/dialog/index.tsx | 3 -- .../app/navigation/navigation-stack.tsx | 41 ++++++++++++++++--- .../mobile/app/screens/add-reminder/index.tsx | 20 ++++++++- 3 files changed, 54 insertions(+), 10 deletions(-) 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/navigation/navigation-stack.tsx b/apps/mobile/app/navigation/navigation-stack.tsx index 486bedf14..f1255a6aa 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -29,11 +29,14 @@ import { useSelectionStore } from "../stores/use-selection-store"; import { useSettingStore } from "../stores/use-setting-store"; 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"; const RootStack = createNativeStackNavigator(); const AppStack = createNativeStackNavigator(); @@ -307,6 +310,7 @@ export const RootNavigation = () => { const pendingShortcutLoaded = useSettingStore( (state) => state.pendingShortcutLoaded ); + const reminderFeature = useIsFeatureAvailable("activeReminders"); const clearSelection = useSelectionStore((state) => state.clearSelection); const resetTimer = React.useRef(undefined); const isNavigationLoaded = React.useRef(true); @@ -338,18 +342,43 @@ export const RootNavigation = () => { if (!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); useSettingStore.setState({ pendingShortcut: null }); } else if (pendingShortcut.type === "notesnook.action.newnote") { - rootNavigatorRef.current?.navigate("FluidPanelsView" as any, { - initialPage: !fluidTabsRef.current ? "editor" : undefined - }); + rootNavigatorRef.current?.navigate("FluidPanelsView" as any); - if (fluidTabsRef.current) { + const runNoteLoad = () => { eSendEvent(eOnLoadNote, { newNote: true }); editorState().movedAway = false; - fluidTabsRef.current.goToPage("editor", true); + fluidTabsRef.current?.goToPage("editor", true); useSettingStore.setState({ pendingShortcut: null }); + }; + + if (fluidTabsRef.current) { + runNoteLoad(); + } else { + const unsub = useSettingStore.subscribe((state) => { + if (state.deviceMode && fluidTabsRef.current) { + unsub(); + runNoteLoad(); + } + }); } } }, [pendingShortcut]); diff --git a/apps/mobile/app/screens/add-reminder/index.tsx b/apps/mobile/app/screens/add-reminder/index.tsx index d5aadf7c6..30c29995b 100644 --- a/apps/mobile/app/screens/add-reminder/index.tsx +++ b/apps/mobile/app/screens/add-reminder/index.tsx @@ -64,6 +64,7 @@ import FormInput, { validators } from "../../components/ui/input/form-input"; import AppIcon from "../../components/ui/AppIcon"; +import { presentDialog } from "../../components/dialog/functions"; const ReminderModes = Platform.OS === "ios" @@ -145,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 || "", @@ -171,6 +173,23 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { ); const [dateError, setDateError] = useState(); const [selectDayError, setSelectDayError] = useState(); + 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); @@ -291,7 +310,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { onPress: saveReminder }} /> - Date: Fri, 17 Jul 2026 09:44:25 +0500 Subject: [PATCH 07/39] mobile: simplified new editor load logic Signed-off-by: kashaf-ansari-dev --- .../app/navigation/navigation-stack.tsx | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/apps/mobile/app/navigation/navigation-stack.tsx b/apps/mobile/app/navigation/navigation-stack.tsx index f1255a6aa..d590c7ac1 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -37,6 +37,7 @@ import { eOnLoadNote } from "../utils/events"; import { strings } from "@notesnook/intl"; import PaywallSheet from "../components/sheets/paywall"; import { presentDialog } from "../components/dialog/functions"; +import { useTabStore } from "../screens/editor/tiptap/use-tab-store"; const RootStack = createNativeStackNavigator(); const AppStack = createNativeStackNavigator(); @@ -361,25 +362,33 @@ export const RootNavigation = () => { rootNavigatorRef.current?.navigate("AddReminder" as any); useSettingStore.setState({ pendingShortcut: null }); } else if (pendingShortcut.type === "notesnook.action.newnote") { - rootNavigatorRef.current?.navigate("FluidPanelsView" as any); - - const runNoteLoad = () => { - eSendEvent(eOnLoadNote, { newNote: true }); - editorState().movedAway = false; - fluidTabsRef.current?.goToPage("editor", true); - useSettingStore.setState({ pendingShortcut: null }); - }; + let tabId; if (fluidTabsRef.current) { - runNoteLoad(); + rootNavigatorRef.current?.navigate("FluidPanelsView" as any); + eSendEvent(eOnLoadNote, { newNote: true }); + editorState().movedAway = false; + fluidTabsRef.current.goToPage("editor", true); } else { - const unsub = useSettingStore.subscribe((state) => { - if (state.deviceMode && fluidTabsRef.current) { - unsub(); - runNoteLoad(); + 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, {}); } + } + + rootNavigatorRef.current?.navigate("FluidPanelsView" as any, { + initialPage: "editor" }); } + + useSettingStore.setState({ pendingShortcut: null }); } }, [pendingShortcut]); From ca19fd923099de46b6b8509d9e5fc7d2a1e9a3bb Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Tue, 21 Jul 2026 17:41:24 +0500 Subject: [PATCH 08/39] mobile: fix new note loading from shortcut on cold launch --- apps/mobile/app/app.tsx | 5 +++++ apps/mobile/app/hooks/use-shortcut-manager.ts | 18 ++++++++++++++++++ .../mobile/app/navigation/navigation-stack.tsx | 18 ++---------------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/apps/mobile/app/app.tsx b/apps/mobile/app/app.tsx index e549ae93a..bee37dbe5 100644 --- a/apps/mobile/app/app.tsx +++ b/apps/mobile/app/app.tsx @@ -46,6 +46,7 @@ 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"; @@ -59,6 +60,10 @@ if (appLockEnabled || appLockMode !== "none") { } Shortcuts.getInitialShortcut().then((shortcut) => { + if (shortcut?.type === "notesnook.action.newnote") { + launchNewNoteTab(); + } + useSettingStore.setState({ pendingShortcut: shortcut ?? null, pendingShortcutLoaded: true diff --git a/apps/mobile/app/hooks/use-shortcut-manager.ts b/apps/mobile/app/hooks/use-shortcut-manager.ts index 224ac9a09..9b990560c 100644 --- a/apps/mobile/app/hooks/use-shortcut-manager.ts +++ b/apps/mobile/app/hooks/use-shortcut-manager.ts @@ -21,6 +21,7 @@ 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 @@ -59,7 +60,24 @@ export function initShortcutListener() { 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/navigation-stack.tsx b/apps/mobile/app/navigation/navigation-stack.tsx index d590c7ac1..ab1ebf957 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -37,7 +37,7 @@ import { eOnLoadNote } from "../utils/events"; import { strings } from "@notesnook/intl"; import PaywallSheet from "../components/sheets/paywall"; import { presentDialog } from "../components/dialog/functions"; -import { useTabStore } from "../screens/editor/tiptap/use-tab-store"; +import { launchNewNoteTab } from "../hooks/use-shortcut-manager"; const RootStack = createNativeStackNavigator(); const AppStack = createNativeStackNavigator(); @@ -358,30 +358,16 @@ export const RootNavigation = () => { useSettingStore.setState({ pendingShortcut: null }); return; } - rootNavigatorRef.current?.navigate("AddReminder" as any); useSettingStore.setState({ pendingShortcut: null }); } else if (pendingShortcut.type === "notesnook.action.newnote") { - let tabId; - if (fluidTabsRef.current) { rootNavigatorRef.current?.navigate("FluidPanelsView" as any); eSendEvent(eOnLoadNote, { newNote: true }); editorState().movedAway = false; fluidTabsRef.current.goToPage("editor", true); } else { - 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, {}); - } - } + launchNewNoteTab(); rootNavigatorRef.current?.navigate("FluidPanelsView" as any, { initialPage: "editor" From 97749729adbeb3a1a751a37be971dcee754294c4 Mon Sep 17 00:00:00 2001 From: kashaf-ansari-dev Date: Tue, 21 Jul 2026 22:03:30 +0500 Subject: [PATCH 09/39] mobile: improve startup loading shortcuts Signed-off-by: kashaf-ansari-dev --- apps/mobile/app/app.tsx | 55 +++++++---- .../app/navigation/fluid-panels-view.tsx | 9 ++ .../app/navigation/navigation-stack.tsx | 97 +++++++++---------- .../mobile/app/screens/add-reminder/index.tsx | 8 ++ apps/mobile/app/stores/use-setting-store.ts | 4 +- 5 files changed, 101 insertions(+), 72 deletions(-) diff --git a/apps/mobile/app/app.tsx b/apps/mobile/app/app.tsx index bee37dbe5..40d17fe49 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"; @@ -59,23 +59,6 @@ if (appLockEnabled || appLockMode !== "none") { useUserStore.getState().lockApp(true); } -Shortcuts.getInitialShortcut().then((shortcut) => { - if (shortcut?.type === "notesnook.action.newnote") { - launchNewNoteTab(); - } - - useSettingStore.setState({ - pendingShortcut: shortcut ?? null, - pendingShortcutLoaded: true - }); - RNBootSplash.hide({ fade: true }); - initShortcutListener(); -}); - -Linking.getInitialURL().then((url) => { - useSettingStore.setState({ initialUrl: url }); -}); - const App = (props: { configureMode: "note-preview" }) => { useAppEvents(); //@ts-ignore @@ -203,4 +186,38 @@ 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() { + const [url, shortcut] = await Promise.all([ + Linking.getInitialURL(), + Shortcuts.getInitialShortcut() + ]); + if (shortcut?.type === "notesnook.action.newnote") { + launchNewNoteTab(); + } + useSettingStore.setState({ + initialUrl: url, + pendingShortcut: shortcut ?? null + }); + + initShortcutListener(); + await RNBootSplash.hide({ fade: true }); + setReady(true); + } + + init(); + }, []); + + if (!ready) return null; + + return ; + }; +}; + +export default withStartupBoundry(withTheme(withErrorBoundry(App, "App"))); diff --git a/apps/mobile/app/navigation/fluid-panels-view.tsx b/apps/mobile/app/navigation/fluid-panels-view.tsx index 1292c6452..e2650aef9 100644 --- a/apps/mobile/app/navigation/fluid-panels-view.tsx +++ b/apps/mobile/app/navigation/fluid-panels-view.tsx @@ -101,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) { diff --git a/apps/mobile/app/navigation/navigation-stack.tsx b/apps/mobile/app/navigation/navigation-stack.tsx index ab1ebf957..486b4150c 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -307,14 +307,15 @@ export const RootNavigation = () => { const introCompleted = useSettingStore( (state) => state.settings.introCompleted ); - const pendingShortcut = useSettingStore((state) => state.pendingShortcut); - const pendingShortcutLoaded = useSettingStore( - (state) => state.pendingShortcutLoaded - ); + + const initialShortcut = React.useRef( + useSettingStore.getState().pendingShortcut + ).current; + const reminderFeature = useIsFeatureAvailable("activeReminders"); const clearSelection = useSelectionStore((state) => state.clearSelection); const resetTimer = React.useRef(undefined); - const isNavigationLoaded = React.useRef(true); + const onStateChange = React.useCallback( (state: any) => { if (useSelectionStore.getState().selectionMode) { @@ -330,59 +331,55 @@ export const RootNavigation = () => { ); React.useEffect(() => { - if (pendingShortcut?.type === "notesnook.action.newreminder") { - useSettingStore.setState({ pendingShortcut: null }); - } - }, []); + const unsubscribe = useSettingStore.subscribe((state, prevState) => { + const pendingShortcut = state.pendingShortcut; - React.useEffect(() => { - if (isNavigationLoaded.current) { - isNavigationLoaded.current = false; - return; - } - if (!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 }); + if (pendingShortcut === prevState.pendingShortcut || !pendingShortcut) { return; } - rootNavigatorRef.current?.navigate("AddReminder" as any); - useSettingStore.setState({ pendingShortcut: null }); - } 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" - }); + 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" + }); + } } + }); - useSettingStore.setState({ pendingShortcut: null }); - } - }, [pendingShortcut]); - - if (!pendingShortcutLoaded) return null; + return unsubscribe; + }, [reminderFeature]); const initialRouteName = !introCompleted ? "Welcome" - : pendingShortcut?.type === "notesnook.action.newreminder" + : initialShortcut?.type === "notesnook.action.newreminder" ? "AddReminder" : "FluidPanelsView"; @@ -419,7 +416,7 @@ export const RootNavigation = () => { }} initialParams={{ initialPage: - pendingShortcut?.type === "notesnook.action.newnote" + initialShortcut?.type === "notesnook.action.newnote" ? "editor" : undefined }} diff --git a/apps/mobile/app/screens/add-reminder/index.tsx b/apps/mobile/app/screens/add-reminder/index.tsx index 30c29995b..e6cc813fa 100644 --- a/apps/mobile/app/screens/add-reminder/index.tsx +++ b/apps/mobile/app/screens/add-reminder/index.tsx @@ -173,6 +173,14 @@ 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) { diff --git a/apps/mobile/app/stores/use-setting-store.ts b/apps/mobile/app/stores/use-setting-store.ts index 1726e7bb9..ea5aa5c28 100644 --- a/apps/mobile/app/stores/use-setting-store.ts +++ b/apps/mobile/app/stores/use-setting-store.ts @@ -151,7 +151,6 @@ export interface SettingStore { inboxEnabled: boolean; setInboxEnabled: (inboxEnabled: boolean) => void; pendingShortcut: ShortcutItem | null; - pendingShortcutLoaded: boolean; } const { width, height } = Dimensions.get("window"); @@ -273,6 +272,5 @@ export const useSettingStore = create((set, get) => ({ }, inboxEnabled: false, setInboxEnabled: (inboxEnabled) => set({ inboxEnabled }), - pendingShortcut: null, - pendingShortcutLoaded: false + pendingShortcut: null })); From e4fa0e9f0b3ffff6eb1c7b6c4125418af4194000 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Sat, 25 Jul 2026 08:54:44 +0500 Subject: [PATCH 10/39] mobile: fix ios app does not load if awaited RNBootSplash.hide --- apps/mobile/app/app.tsx | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/mobile/app/app.tsx b/apps/mobile/app/app.tsx index 40d17fe49..96b8a6fe7 100644 --- a/apps/mobile/app/app.tsx +++ b/apps/mobile/app/app.tsx @@ -74,6 +74,7 @@ const App = (props: { configureMode: "note-preview" }) => { }, [introCompleted]); useEffect(() => { + RNBootSplash.hide({ fade: true }); SettingsService.onFirstLaunch(); changeSystemBarColors(); SettingsService.setPrivacyScreen( @@ -194,21 +195,24 @@ export const withStartupBoundry = ( useEffect(() => { async function init() { - const [url, shortcut] = await Promise.all([ - Linking.getInitialURL(), - Shortcuts.getInitialShortcut() - ]); - if (shortcut?.type === "notesnook.action.newnote") { - launchNewNoteTab(); - } - useSettingStore.setState({ - initialUrl: url, - pendingShortcut: shortcut ?? null - }); + 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(); - await RNBootSplash.hide({ fade: true }); - setReady(true); + initShortcutListener(); + } finally { + setReady(true); + } } init(); From 0939f0f8059c6ac4bca00fc942e1c5f6cb954877 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 09:56:18 +0500 Subject: [PATCH 11/39] mobile: migrated off the deprecated background-activity-launch mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API 36+ → MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE API 34–35 → MODE_BACKGROUND_ACTIVITY_START_ALLOWED (unchanged behaviour on older devices) < 34 → null --- .../notesnook/NotePreviewWidget.java | 14 +------- .../streetwriters/notesnook/NoteWidget.java | 14 +------- .../notesnook/ReminderWidgetProvider.java | 17 ++------- .../streetwriters/notesnook/WidgetUtils.java | 36 +++++++++++++++++++ 4 files changed, 40 insertions(+), 41 deletions(-) create mode 100644 apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java index 831eaaa87..111a4b23a 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java @@ -1,6 +1,5 @@ package com.streetwriters.notesnook; -import android.app.ActivityOptions; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; @@ -8,7 +7,6 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.widget.RemoteViews; import com.google.gson.Gson; @@ -34,7 +32,7 @@ public class NotePreviewWidget extends AppWidgetProvider { intent.setAction(Intent.ACTION_VIEW); intent.putExtra(RCTNNativeModule.IntentType, "OpenNote"); intent.setData(Uri.parse("nn://note/" + note.getId())); - PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setOnClickPendingIntent(R.id.open_note, pendingIntent); appWidgetManager.updateAppWidget(appWidgetId, views); @@ -45,16 +43,6 @@ public class NotePreviewWidget extends AppWidgetProvider { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); } - private static Bundle getActivityOptionsBundle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ActivityOptions activityOptions = ActivityOptions.makeBasic(); - activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - return activityOptions.toBundle(); - } else - return null; - } - @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java index 6207f5ef1..6efe9ed1e 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java @@ -1,12 +1,10 @@ package com.streetwriters.notesnook; -import android.app.ActivityOptions; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.widget.RemoteViews; @@ -24,20 +22,10 @@ public class NoteWidget extends AppWidgetProvider { static void setClickIntent(Context context, RemoteViews views) { Intent intent = new Intent(context, ShareActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setOnClickPendingIntent(R.id.new_note, pendingIntent); } - private static Bundle getActivityOptionsBundle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ActivityOptions activityOptions = ActivityOptions.makeBasic(); - activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - return activityOptions.toBundle(); - } else - return null; - } - @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java index 3ecd4daed..ec62755ca 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java @@ -1,14 +1,11 @@ package com.streetwriters.notesnook; -import android.app.ActivityOptions; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; -import android.os.Bundle; import android.widget.RemoteViews; public class ReminderWidgetProvider extends AppWidgetProvider { @@ -23,20 +20,10 @@ public class ReminderWidgetProvider extends AppWidgetProvider { } - private static Bundle getActivityOptionsBundle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ActivityOptions activityOptions = ActivityOptions.makeBasic(); - activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - return activityOptions.toBundle(); - } else - return null; - } - static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, RemoteViews views) { Intent listview_intent_template = new Intent(context, MainActivity.class); listview_intent_template.setAction(Intent.ACTION_VIEW); - PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent); Intent new_reminder_intent = new Intent(context, MainActivity.class); @@ -44,7 +31,7 @@ public class ReminderWidgetProvider extends AppWidgetProvider { new_reminder_intent.setAction(Intent.ACTION_VIEW); new_reminder_intent.putExtra(RCTNNativeModule.IntentType, "NewReminder"); new_reminder_intent.setData(Uri.parse("https://app.notesnook.com/new_reminder")); - PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setOnClickPendingIntent(R.id.add_button, pendingIntent2); Intent list_remote_adapter_intent = new Intent(context, ReminderViewsService.class); diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java new file mode 100644 index 000000000..0d6b5c7a8 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java @@ -0,0 +1,36 @@ +package com.streetwriters.notesnook; + +import android.app.ActivityOptions; +import android.os.Build; +import android.os.Bundle; + +/** + * Shared helpers for the home screen widgets. + */ +public class WidgetUtils { + + /** + * Options attached to the PendingIntents our widgets hand to the launcher, opting the creator + * (us) in to background activity starts so a tap on the widget can bring up an activity. + * + * MODE_BACKGROUND_ACTIVITY_START_ALLOWED is deprecated since API 36 and Android 17 extends the + * background activity launch restrictions to IntentSender, so on API 36+ we use the narrower + * MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE instead. That is enough for widgets: the + * sender is the launcher, which is visible whenever the user taps the widget. + */ + static Bundle getActivityOptionsBundle() { + ActivityOptions activityOptions; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + } else { + return null; + } + return activityOptions.toBundle(); + } +} From 9b29503b1717d5bd1ea530dfeb80180c500fa9a1 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 10:08:16 +0500 Subject: [PATCH 12/39] mobile: added onRestored() that migrates each stored note in the appPreview prefs from its old widget ID key to the new one. --- .../android/app/src/main/AndroidManifest.xml | 1 + .../notesnook/NotePreviewWidget.java | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 4f46df45c..877df5eca 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -88,6 +88,7 @@ android:label="@string/note"> " + newKeys = new HashSet<>(); + for (int i = 0; i < count; i++) { + notes[i] = preferences.getString(String.valueOf(oldWidgetIds[i]), ""); + newKeys.add(String.valueOf(newWidgetIds[i])); + } + + SharedPreferences.Editor edit = preferences.edit(); + for (int i = 0; i < count; i++) { + String oldKey = String.valueOf(oldWidgetIds[i]); + if (!newKeys.contains(oldKey)) { + edit.remove(oldKey); + } + } + for (int i = 0; i < count; i++) { + if (notes[i].isEmpty()) continue; + edit.putString(String.valueOf(newWidgetIds[i]), notes[i]); + } + edit.apply(); + } + @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); From 343d2b013182717595aec1b2dbf595f74593b982 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 10:39:20 +0500 Subject: [PATCH 13/39] core: export getUpcomingReminderTime --- packages/core/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 68056d730..2d27f43f5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -30,6 +30,7 @@ export { type DatabaseUpdatedEvent } from "./database/index.js"; export { FilteredSelector } from "./database/sql-collection.js"; export { getUpcomingReminder, + getUpcomingReminderTime, formatReminderTime, isReminderToday, isReminderActive From 942c9287fe237b51bd382fd2e1e8417b229ba187 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 10:39:39 +0500 Subject: [PATCH 14/39] =?UTF-8?q?mobile:=20fix=20stale=20reminder=20times?= =?UTF-8?q?=20=E2=80=94=20render-time=20formatting=20+=20valid=20updatePer?= =?UTF-8?q?iodMillis=20+=20self-refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notesnook/ReminderViewsService.java | 21 ++++- .../notesnook/ReminderWidgetProvider.java | 1 + .../streetwriters/notesnook/WidgetUtils.java | 84 +++++++++++++++++++ .../notesnook/datatypes/Reminder.java | 52 ++++++------ .../app/src/main/res/values/strings.xml | 7 ++ .../main/res/xml/widget_reminders_info.xml | 2 +- apps/mobile/app/services/notifications.ts | 21 ++++- 7 files changed, 153 insertions(+), 35 deletions(-) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java index 5e9416e39..1d1a40c4a 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java @@ -42,11 +42,24 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor @Override public void onDataSetChanged() { - reminders.clear(); SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Gson gson = new Gson(); - reminders = gson.fromJson(preferences.getString("remindersList","[]"), new TypeToken>(){}.getType()); + List stored = null; + try { + Gson gson = new Gson(); + stored = gson.fromJson(preferences.getString("remindersList", "[]"), new TypeToken>(){}.getType()); + } catch (Exception e) { + Log.e("Reminders", "Could not read the stored reminders list", e); + } + List updated = new ArrayList(); + if (stored != null) { + for (Reminder reminder : stored) { + if (WidgetUtils.isReminderActive(reminder)) { + updated.add(reminder); + } + } + } + reminders = updated; } @Override @@ -71,7 +84,7 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor if (!useMiniLayout) { views.setTextViewText(R.id.reminder_description, reminder.getDescription()); } - views.setTextViewText(R.id.reminder_time, reminder.getFormattedTime()); + views.setTextViewText(R.id.reminder_time, WidgetUtils.formatReminderTime(context, reminder)); final Intent fillInIntent = new Intent(); final Bundle extras = new Bundle(); extras.putString(ReminderViewsService.OpenReminderId, reminder.getId()); diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java index ec62755ca..dc3aa9ff8 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java @@ -16,6 +16,7 @@ public class ReminderWidgetProvider extends AppWidgetProvider { for (int appWidgetId : appWidgetIds) { RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders); updateAppWidget(context, appWidgetManager, appWidgetId, views); + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list_view); } } diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java index 0d6b5c7a8..109f01403 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java @@ -1,8 +1,14 @@ package com.streetwriters.notesnook; import android.app.ActivityOptions; +import android.content.Context; import android.os.Build; import android.os.Bundle; +import android.text.format.DateUtils; + +import com.streetwriters.notesnook.datatypes.Reminder; + +import java.util.Calendar; /** * Shared helpers for the home screen widgets. @@ -33,4 +39,82 @@ public class WidgetUtils { } return activityOptions.toBundle(); } + + /** + * Whether a reminder should still be listed. Mirrors isReminderActive() in the core package, + * which the app applies when it writes the list out. We re-check here because the stored list + * is only rewritten while the app runs, so one-off reminders would otherwise linger in the + * widget long after they fired. + */ + static boolean isReminderActive(Reminder reminder) { + if (reminder == null) return false; + if (reminder.isDisabled()) return false; + + long now = System.currentTimeMillis(); + if (reminder.getSnoozeUntil() > now) return true; + if (!"once".equals(reminder.getMode())) return true; + + long triggerDate = reminder.getTriggerDate() > 0 ? reminder.getTriggerDate() : reminder.getDate(); + return triggerDate > now; + } + + /** + * Builds the label shown under a reminder. The app sends us the absolute trigger time plus the + * parts that never change ("5:00 PM", "12-05-2026, 5:00 PM"); everything that depends on the + * current time is decided here so it stays right as the widget redraws. + * + * Falls back to the pre-formatted string for lists written by an older version of the app. + */ + static String formatReminderTime(Context context, Reminder reminder) { + long triggerDate = reminder.getTriggerDate(); + String timeOfDay = reminder.getFormattedTimeOfDay(); + if (triggerDate <= 0 || timeOfDay == null || timeOfDay.isEmpty()) { + return reminder.getFormattedTime(); + } + + if ("permanent".equals(reminder.getMode())) { + return context.getString(R.string.reminder_ongoing); + } + + long now = System.currentTimeMillis(); + if (reminder.getSnoozeUntil() > now) { + return context.getString(R.string.reminder_snoozed_until, timeOfDay); + } + + String text; + long dayOffset = daysFromToday(triggerDate, now); + if (dayOffset == 0) { + text = context.getString(R.string.reminder_today, timeOfDay); + } else if (dayOffset == 1) { + text = context.getString(R.string.reminder_tomorrow, timeOfDay); + } else if (dayOffset == -1) { + text = context.getString(R.string.reminder_yesterday, timeOfDay); + } else { + text = reminder.getFormattedDateTime(); + if (text == null || text.isEmpty()) return reminder.getFormattedTime(); + } + + return context.getString( + triggerDate <= now ? R.string.reminder_last : R.string.reminder_upcoming, text); + } + + /** + * Calendar days between two instants. Compares midnights rather than subtracting the raw + * difference so that "tomorrow" is still tomorrow across a DST change or just before midnight. + */ + private static long daysFromToday(long time, long now) { + long target = startOfDay(time); + long today = startOfDay(now); + return Math.round((target - today) / (double) DateUtils.DAY_IN_MILLIS); + } + + private static long startOfDay(long time) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(time); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTimeInMillis(); + } } diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java index 715199ca6..9b9b0fac6 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java @@ -2,13 +2,14 @@ package com.streetwriters.notesnook.datatypes; import androidx.annotation.Keep; -import java.util.concurrent.TimeUnit; - @Keep public class Reminder extends BaseItem { private String title; private String description; private String formattedTime; + private String formattedTimeOfDay; // e.g. "5:00 PM" + private String formattedDateTime; // e.g. "12-05-2026, 5:00 PM" + private long triggerDate; // absolute time this reminder next fires private String priority; // "silent", "vibrate", "urgent" private long date; private String mode; // "repeat", "once", "permanent" @@ -107,32 +108,27 @@ public class Reminder extends BaseItem { this.formattedTime = formattedTime; } - public String formatTime(long timeInMillis) { - long currentTime = System.currentTimeMillis(); - long diff = timeInMillis - currentTime; + public String getFormattedTimeOfDay() { + return formattedTimeOfDay; + } - if (diff < TimeUnit.MINUTES.toMillis(1)) { - return "in " + (diff / 1000) + " seconds"; - } else if (diff < TimeUnit.HOURS.toMillis(1)) { - long minutes = TimeUnit.MILLISECONDS.toMinutes(diff); - return "in " + minutes + " minute" + (minutes > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(1)) { - long hours = TimeUnit.MILLISECONDS.toHours(diff); - return "in " + hours + " hour" + (hours > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(2)) { - return "tomorrow"; - } else if (diff < TimeUnit.DAYS.toMillis(7)) { - long days = TimeUnit.MILLISECONDS.toDays(diff); - return "in " + days + " day" + (days > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(30)) { - long weeks = TimeUnit.MILLISECONDS.toDays(diff) / 7; - return "in " + weeks + " week" + (weeks > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(365)) { - long months = TimeUnit.MILLISECONDS.toDays(diff) / 30; - return "in " + months + " month" + (months > 1 ? "s" : ""); - } else { - long years = TimeUnit.MILLISECONDS.toDays(diff) / 365; - return "in " + years + " year" + (years > 1 ? "s" : ""); - } + public void setFormattedTimeOfDay(String formattedTimeOfDay) { + this.formattedTimeOfDay = formattedTimeOfDay; + } + + public String getFormattedDateTime() { + return formattedDateTime; + } + + public void setFormattedDateTime(String formattedDateTime) { + this.formattedDateTime = formattedDateTime; + } + + public long getTriggerDate() { + return triggerDate; + } + + public void setTriggerDate(long triggerDate) { + this.triggerDate = triggerDate; } } \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index 1a233ee09..a9ab7f599 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -9,4 +9,11 @@ Note Add a note to home screen Quick note + Ongoing + Snoozed until %1$s + Today, %1$s + Tomorrow, %1$s + Yesterday, %1$s + Upcoming: %1$s + Last: %1$s diff --git a/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml b/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml index e9c87362f..d88c14187 100644 --- a/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml @@ -9,6 +9,6 @@ android:targetCellHeight="2" android:resizeMode="horizontal|vertical" android:previewImage="@drawable/reminder_preview" - android:updatePeriodMillis="1024" + android:updatePeriodMillis="1800000" android:widgetCategory="home_screen" /> \ No newline at end of file diff --git a/apps/mobile/app/services/notifications.ts b/apps/mobile/app/services/notifications.ts index 73ffa8e45..389beca39 100644 --- a/apps/mobile/app/services/notifications.ts +++ b/apps/mobile/app/services/notifications.ts @@ -17,8 +17,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { getFormattedReminderTime } from "@notesnook/common"; -import { isReminderActive, Reminder } from "@notesnook/core"; +import { getFormattedDate, getFormattedReminderTime } from "@notesnook/common"; +import { + getUpcomingReminderTime, + isReminderActive, + Reminder +} from "@notesnook/core"; import { strings } from "@notesnook/intl"; import notifee, { AndroidStyle, @@ -262,6 +266,9 @@ const onEvent = async ({ type, detail }: Event) => { type ReminderWithFormattedTime = Reminder & { formattedTime?: string; + triggerDate?: number; + formattedTimeOfDay?: string; + formattedDateTime?: string; }; async function updateRemindersForWidget() { @@ -277,6 +284,16 @@ async function updateRemindersForWidget() { if (!reminders) return; for (const reminder of reminders) { if (isReminderActive(reminder)) { + const triggerDate = + reminder.snoozeUntil && reminder.snoozeUntil > Date.now() + ? reminder.snoozeUntil + : reminder.mode === "repeat" + ? getUpcomingReminderTime(reminder) + : reminder.date; + + reminder.triggerDate = triggerDate; + reminder.formattedTimeOfDay = getFormattedDate(triggerDate, "time"); + reminder.formattedDateTime = getFormattedDate(triggerDate, "date-time"); reminder.formattedTime = getFormattedReminderTime(reminder); activeReminders.push(reminder); } From 88eb3ab714eb88c71b8a0aa0709e7611a99ad800 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 10:49:36 +0500 Subject: [PATCH 15/39] =?UTF-8?q?mobile:=20fix=20NotePreviewWidget=20empty?= =?UTF-8?q?=20state=20has=20no=20click=20handler=20=E2=80=94=20make=20it?= =?UTF-8?q?=20tap-to-configure=20so=20a=20blank=20widget=20can=20self-heal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotePreviewConfigureActivity.java | 34 ++++++++++----- .../notesnook/NotePreviewWidget.java | 41 +++++++++++++++++-- .../app/src/main/res/layout/note_widget.xml | 4 +- .../app/src/main/res/values/strings.xml | 2 + 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java index a87a1f743..4418ce56f 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java @@ -40,18 +40,32 @@ public class NotePreviewConfigureActivity extends ReactActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(null); - Intent intent = getIntent(); - Bundle extras = intent.getExtras(); - int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; - if (extras != null) { - appWidgetId = extras.getInt( - AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID); - NotePreviewConfigureActivity.appWidgetId = appWidgetId; - } + activity = this; + readAppWidgetId(getIntent()); + } + + /** + * We launch as singleTask, so configuring a second widget while this screen is still alive + * arrives here rather than in onCreate(). Without this the activity would keep writing to + * whichever widget it happened to be opened for first. + */ + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + activity = this; + readAppWidgetId(intent); + } + + private void readAppWidgetId(Intent intent) { + Bundle extras = intent != null ? intent.getExtras() : null; + int appWidgetId = extras != null + ? extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + : AppWidgetManager.INVALID_APPWIDGET_ID; + + NotePreviewConfigureActivity.appWidgetId = appWidgetId; Intent resultValue = new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); setResult(Activity.RESULT_CANCELED, resultValue); - activity = this; } public static void saveAndFinish(Context context) { diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java index f153e9f3b..14117db71 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java @@ -8,6 +8,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.util.Log; import android.widget.RemoteViews; import com.google.gson.Gson; import com.streetwriters.notesnook.datatypes.Note; @@ -21,12 +22,28 @@ public class NotePreviewWidget extends AppWidgetProvider { static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { String data = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), ""); - if (data.isEmpty()) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget); + + Note note = null; + if (data != null && !data.isEmpty()) { + try { + note = new Gson().fromJson(data, Note.class); + } catch (Exception e) { + Log.e("NotePreviewWidget", "Could not read the note stored for widget " + appWidgetId, e); + } + } + + if (note == null) { + // Either the widget was never configured, or we lost the note it pointed at (ids + // reassigned, data cleared). Point it back at the picker rather than leaving the user + // with an inert widget they can only fix by deleting and re-adding it. + views.setTextViewText(R.id.widget_title, context.getString(R.string.widget_note_unconfigured_title)); + views.setTextViewText(R.id.widget_body, context.getString(R.string.widget_note_unconfigured_body)); + views.setOnClickPendingIntent(R.id.open_note, getConfigurePendingIntent(context, appWidgetId)); + appWidgetManager.updateAppWidget(appWidgetId, views); return; } - Gson gson = new Gson(); - Note note = gson.fromJson(data, Note.class); - RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget); + views.setTextViewText(R.id.widget_title, note.getTitle()); views.setTextViewText(R.id.widget_body, note.getHeadline()); @@ -41,6 +58,22 @@ public class NotePreviewWidget extends AppWidgetProvider { appWidgetManager.updateAppWidget(appWidgetId, views); } + /** + * Reopens the configure screen for this widget. The launcher's own "reconfigure" gesture is + * hard to discover and not offered by every launcher, so an unconfigured widget needs its own + * way back in. + */ + private static PendingIntent getConfigurePendingIntent(Context context, int appWidgetId) { + Intent intent = new Intent(context, NotePreviewConfigureActivity.class); + intent.setAction(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + // PendingIntent equality ignores extras, so the widget id has to be the request code for + // each widget to get its own. + return PendingIntent.getActivity(context, appWidgetId, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, + WidgetUtils.getActivityOptionsBundle()); + } + @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); diff --git a/apps/mobile/android/app/src/main/res/layout/note_widget.xml b/apps/mobile/android/app/src/main/res/layout/note_widget.xml index 8a97ce0d3..2eecacfb2 100644 --- a/apps/mobile/android/app/src/main/res/layout/note_widget.xml +++ b/apps/mobile/android/app/src/main/res/layout/note_widget.xml @@ -25,7 +25,7 @@ android:textColor="@color/text" android:textSize="16sp" android:textStyle="bold" - android:text="Widget unconfigured" /> + android:text="@string/widget_note_unconfigured_title" /> + android:text="@string/widget_note_unconfigured_body" /> \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index a9ab7f599..7a86c8d38 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -9,6 +9,8 @@ Note Add a note to home screen Quick note + Tap to choose a note + Pick the note you want shown here. Ongoing Snoozed until %1$s Today, %1$s From b2c7f732a0d71746bd7bd72c835959b6ea47947e Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 11:10:46 +0500 Subject: [PATCH 16/39] mobile: replace reminders widget service with RemoteCollectionItems Push the reminder rows into the widget update itself via RemoteViewsCompat instead of binding a RemoteViewsService adapter, which has been deprecated since API 31. Removes the bound service entirely, so there is no cached factory to invalidate and no separate notifyAppWidgetViewDataChanged step that can race or be missed. Adds androidx.core:core-remoteviews for the API 24-30 fallback. Row ids are now derived from the reminder id rather than the list position, and the list is capped at 50 rows so the update fits in a binder transaction. --- apps/mobile/android/app/build.gradle | 3 + .../android/app/src/main/AndroidManifest.xml | 5 - .../notesnook/RCTNNativeModule.java | 4 +- .../notesnook/ReminderViewsService.java | 119 ------------------ .../notesnook/ReminderWidgetProvider.java | 34 ++++- .../streetwriters/notesnook/WidgetUtils.java | 69 ++++++++++ 6 files changed, 103 insertions(+), 131 deletions(-) delete mode 100644 apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 7cc709a78..1f4781422 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -233,6 +233,9 @@ dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") + // Lets the widgets push their list data straight into the RemoteViews on every API level, + // instead of the deprecated RemoteViewsService adapter (which needs API 31 to do natively). + implementation("androidx.core:core-remoteviews:1.0.0") implementation("androidx.core:core-splashscreen:1.0.0") implementation 'androidx.multidex:multidex:2.0.1' diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 877df5eca..fc7f88466 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -231,11 +231,6 @@ - - reminders; - - public ReminderRemoteViewsFactory(Context context, Intent intent) { - this.context = context; - } - - @Override - public void onCreate() { - // Initialize reminders list - reminders = new ArrayList(); - } - - @Override - public void onDataSetChanged() { - SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE); - List stored = null; - try { - Gson gson = new Gson(); - stored = gson.fromJson(preferences.getString("remindersList", "[]"), new TypeToken>(){}.getType()); - } catch (Exception e) { - Log.e("Reminders", "Could not read the stored reminders list", e); - } - - List updated = new ArrayList(); - if (stored != null) { - for (Reminder reminder : stored) { - if (WidgetUtils.isReminderActive(reminder)) { - updated.add(reminder); - } - } - } - reminders = updated; - } - - @Override - public void onDestroy() { - reminders.clear(); - } - - @Override - public int getCount() { - return reminders.size(); - } - - @Override - public RemoteViews getViewAt(int position) { - Reminder reminder = reminders.get(position); - - boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty(); - - RemoteViews views = new RemoteViews(context.getPackageName(), useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout); - - views.setTextViewText(R.id.reminder_title, reminder.getTitle()); - if (!useMiniLayout) { - views.setTextViewText(R.id.reminder_description, reminder.getDescription()); - } - views.setTextViewText(R.id.reminder_time, WidgetUtils.formatReminderTime(context, reminder)); - final Intent fillInIntent = new Intent(); - final Bundle extras = new Bundle(); - extras.putString(ReminderViewsService.OpenReminderId, reminder.getId()); - fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId())); - fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder"); - fillInIntent.putExtras(extras); - views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent); - return views; - } - - - @Override - public RemoteViews getLoadingView() { - return null; - } - - @Override - public int getViewTypeCount() { - return 2; - } - - @Override - public long getItemId(int position) { - - return position; - } - - @Override - public boolean hasStableIds() { - return true; - } -} \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java index dc3aa9ff8..4619f594c 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java @@ -8,15 +8,21 @@ import android.content.Intent; import android.net.Uri; import android.widget.RemoteViews; +import androidx.core.widget.RemoteViewsCompat; + +import com.streetwriters.notesnook.datatypes.Reminder; + +import java.util.List; + public class ReminderWidgetProvider extends AppWidgetProvider { static String NewReminder = "com.streetwriters.notesnook.NewReminder"; + static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId"; @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int appWidgetId : appWidgetIds) { RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders); updateAppWidget(context, appWidgetManager, appWidgetId, views); - appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list_view); } } @@ -35,10 +41,28 @@ public class ReminderWidgetProvider extends AppWidgetProvider { PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setOnClickPendingIntent(R.id.add_button, pendingIntent2); - Intent list_remote_adapter_intent = new Intent(context, ReminderViewsService.class); - list_remote_adapter_intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - views.setRemoteAdapter(R.id.widget_list_view, list_remote_adapter_intent); + // The rows travel with the update itself, so there is no bound service to keep in sync and + // nothing to invalidate separately: every update redraws from the current data. + List reminders = WidgetUtils.getActiveReminders(context); + RemoteViewsCompat.RemoteCollectionItems.Builder items = + new RemoteViewsCompat.RemoteCollectionItems.Builder(); + for (Reminder reminder : reminders) { + items.addItem(getItemId(reminder), WidgetUtils.createReminderItem(context, reminder)); + } + // Two, because a reminder without a description uses the compact row layout. + items.setViewTypeCount(2); + items.setHasStableIds(true); + + RemoteViewsCompat.setRemoteAdapter(context, views, appWidgetId, R.id.widget_list_view, items.build()); views.setEmptyView(R.id.widget_list_view, R.id.empty_view); appWidgetManager.updateAppWidget(appWidgetId, views); } -} \ No newline at end of file + + /** + * Ties a row to its reminder rather than to its position, so rows keep their identity when the + * list shifts around them. + */ + private static long getItemId(Reminder reminder) { + return reminder.getId() == null ? 0 : reminder.getId().hashCode(); + } +} diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java index 109f01403..fa1d053a2 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java @@ -2,19 +2,88 @@ package com.streetwriters.notesnook; import android.app.ActivityOptions; import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.format.DateUtils; +import android.util.Log; +import android.widget.RemoteViews; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import com.streetwriters.notesnook.datatypes.Reminder; +import java.util.ArrayList; import java.util.Calendar; +import java.util.List; /** * Shared helpers for the home screen widgets. */ public class WidgetUtils { + static final String PREFERENCES = "appPreview"; + static final String REMINDERS_KEY = "remindersList"; + + /** + * Every row is serialized into the widget update itself, which has to fit inside a binder + * transaction, so the list cannot grow without bound. Far more than fits on screen anyway. + */ + private static final int MAX_REMINDERS = 50; + + /** + * The reminders the app last wrote out, minus any that have since fired. Reading and filtering + * happens here so the provider can push the rows straight into the widget. + */ + static List getActiveReminders(Context context) { + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + List stored = null; + try { + stored = new Gson().fromJson(preferences.getString(REMINDERS_KEY, "[]"), + new TypeToken>() {}.getType()); + } catch (Exception e) { + Log.e("Reminders", "Could not read the stored reminders list", e); + } + + List active = new ArrayList<>(); + if (stored == null) return active; + + for (Reminder reminder : stored) { + if (!isReminderActive(reminder)) continue; + if (active.size() >= MAX_REMINDERS) { + Log.w("Reminders", "Widget list truncated to " + MAX_REMINDERS + " reminders"); + break; + } + active.add(reminder); + } + return active; + } + + /** + * Builds a single row of the reminders list. + */ + static RemoteViews createReminderItem(Context context, Reminder reminder) { + boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty(); + + RemoteViews views = new RemoteViews(context.getPackageName(), + useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout); + + views.setTextViewText(R.id.reminder_title, reminder.getTitle()); + if (!useMiniLayout) { + views.setTextViewText(R.id.reminder_description, reminder.getDescription()); + } + views.setTextViewText(R.id.reminder_time, formatReminderTime(context, reminder)); + + Intent fillInIntent = new Intent(); + fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId())); + fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder"); + fillInIntent.putExtra(ReminderWidgetProvider.OpenReminderId, reminder.getId()); + views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent); + return views; + } + /** * Options attached to the PendingIntents our widgets hand to the launcher, opting the creator * (us) in to background activity starts so a tap on the widget can bring up an activity. From 55b51b81e364e3b6affc9dbb4313a52f2cf4e0ba Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 11:17:12 +0500 Subject: [PATCH 17/39] mobile: add previewLayout for all widgets --- .../main/res/layout/note_widget_preview.xml | 42 ++++++++++ .../res/layout/widget_reminders_preview.xml | 82 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 8 ++ .../src/main/res/xml/new_note_widget_info.xml | 1 + .../app/src/main/res/xml/note_widget_info.xml | 1 + .../main/res/xml/widget_reminders_info.xml | 1 + 6 files changed, 135 insertions(+) create mode 100644 apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml create mode 100644 apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml diff --git a/apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml b/apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml new file mode 100644 index 000000000..710b74aa9 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml new file mode 100644 index 000000000..2208f51fd --- /dev/null +++ b/apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index 7a86c8d38..8652b200d 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -9,6 +9,14 @@ Note Add a note to home screen Quick note + + Meeting notes + Discuss the roadmap and agree on timelines. + Take a walk + Upcoming: Today, 5:00 PM + Call the dentist + Upcoming: Tomorrow, 9:00 AM + Tap to choose a note Pick the note you want shown here. Ongoing diff --git a/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml b/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml index 086bf7228..c76d158b6 100644 --- a/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml @@ -10,6 +10,7 @@ android:targetCellWidth="5" android:targetCellHeight="1" android:previewImage="@drawable/widget_preview" + android:previewLayout="@layout/new_note_widget" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen"/> \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml b/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml index 84c936b29..c597dc450 100644 --- a/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml @@ -12,6 +12,7 @@ android:targetCellWidth="5" android:targetCellHeight="1" android:previewImage="@drawable/note_widget_preview" + android:previewLayout="@layout/note_widget_preview" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen"/> \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml b/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml index d88c14187..0d7068b1c 100644 --- a/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml @@ -9,6 +9,7 @@ android:targetCellHeight="2" android:resizeMode="horizontal|vertical" android:previewImage="@drawable/reminder_preview" + android:previewLayout="@layout/widget_reminders_preview" android:updatePeriodMillis="1800000" android:widgetCategory="home_screen" /> \ No newline at end of file From 6b5e6f62c5fd2ff10892bf2376e911da57b54461 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 11:25:34 +0500 Subject: [PATCH 18/39] mobile: make NotePreviewWidget respond to resize events and properly resize based on available room. --- .../notesnook/NotePreviewWidget.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java index 14117db71..66c22778d 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java @@ -9,6 +9,7 @@ import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.util.Log; +import android.view.View; import android.widget.RemoteViews; import com.google.gson.Gson; import com.streetwriters.notesnook.datatypes.Note; @@ -46,6 +47,10 @@ public class NotePreviewWidget extends AppWidgetProvider { views.setTextViewText(R.id.widget_title, note.getTitle()); views.setTextViewText(R.id.widget_body, note.getHeadline()); + // Once the user shrinks the widget down to a single row there is no room for the preview + // text, and a clipped half-line of it looks like a rendering glitch. + views.setViewVisibility(R.id.widget_body, + hasRoomForBody(appWidgetManager, appWidgetId) ? View.VISIBLE : View.GONE); Intent intent = new Intent(context, MainActivity.class); intent.putExtra(OpenNoteId, note.getId()); @@ -74,9 +79,25 @@ public class NotePreviewWidget extends AppWidgetProvider { WidgetUtils.getActivityOptionsBundle()); } + /** + * Height below which the note preview text is dropped, leaving just the title. + */ + private static final int MIN_HEIGHT_FOR_BODY_DP = 70; + + private static boolean hasRoomForBody(AppWidgetManager appWidgetManager, int appWidgetId) { + Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId); + if (options == null) return true; + + int minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT); + // Not reported yet (the widget has just been placed): assume there is room. + return minHeight <= 0 || minHeight >= MIN_HEIGHT_FOR_BODY_DP; + } + @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); + // This used to do nothing at all, so resizing the widget left it rendered for its old size. + updateAppWidget(context, appWidgetManager, appWidgetId); } /** From 314c484ea072869bdf550ec1c66dec8136d6957a Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:01:31 +0500 Subject: [PATCH 19/39] mobile: fix note widget lookups corrupting the reminders list updateWidgetNote scanned every key in the appPreview preferences and rewrote any whose value contained the note id. That included the remindersList key, so a match replaced the entire reminders list with a note and then threw NumberFormatException parsing "remindersList" as a widget id. hasWidgetNote had the same unguarded scan. Match on the parsed note's id instead of on the raw JSON containing it, and treat only keys that parse as an int as widget entries, so non-widget keys are excluded structurally rather than by name. Also guards the casts and gson parses that could throw on a malformed entry. --- .../notesnook/NotePreviewWidget.java | 14 +----- .../notesnook/RCTNNativeModule.java | 41 +++++++--------- .../streetwriters/notesnook/WidgetUtils.java | 47 +++++++++++++++++++ 3 files changed, 66 insertions(+), 36 deletions(-) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java index 66c22778d..757187c67 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java @@ -8,10 +8,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; -import android.util.Log; import android.view.View; import android.widget.RemoteViews; -import com.google.gson.Gson; import com.streetwriters.notesnook.datatypes.Note; import java.util.HashSet; @@ -22,18 +20,10 @@ public class NotePreviewWidget extends AppWidgetProvider { static String OpenNoteId = "com.streetwriters.notesnook.OpenNoteId"; static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { - String data = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), ""); + String data = context.getSharedPreferences(WidgetUtils.PREFERENCES, Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), ""); RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget); - Note note = null; - if (data != null && !data.isEmpty()) { - try { - note = new Gson().fromJson(data, Note.class); - } catch (Exception e) { - Log.e("NotePreviewWidget", "Could not read the note stored for widget " + appWidgetId, e); - } - } - + Note note = WidgetUtils.parseNote(data); if (note == null) { // Either the widget was never configured, or we lost the note it pointed at (ids // reassigned, data cleared). Point it back at the picker rather than leaving the user diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java index e5872cfb5..cf21ecbdf 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java @@ -29,7 +29,6 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; -import com.google.gson.Gson; import com.streetwriters.notesnook.datatypes.Note; import java.util.ArrayList; @@ -156,14 +155,8 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { @ReactMethod public void getWidgetNotes(Promise promise) { - SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Map map = pref.getAll(); WritableArray arr = Arguments.createArray(); - for(Map.Entry entry : map.entrySet()){ - if (entry.getKey().equals("remindersList")) continue; - String value = (String) entry.getValue(); - Gson gson = new Gson(); - Note note = gson.fromJson(value, Note.class); + for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) { arr.pushString(note.getId()); } promise.resolve(arr); @@ -171,33 +164,33 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { @ReactMethod public void hasWidgetNote(final String noteId, Promise promise) { - SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Map map = pref.getAll(); boolean found = false; - for(Map.Entry entry : map.entrySet()){ - String value = (String) entry.getValue(); - if (value.contains(noteId)) { + for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) { + if (note.getId().equals(noteId)) { found = true; + break; } } promise.resolve(found); } + @ReactMethod public void updateWidgetNote(final String noteId, final String data) { - SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Map map = pref.getAll(); + SharedPreferences pref = getReactApplicationContext().getSharedPreferences(WidgetUtils.PREFERENCES, Context.MODE_PRIVATE); SharedPreferences.Editor edit = pref.edit(); - ArrayList ids = new ArrayList<>(); - for(Map.Entry entry : map.entrySet()) { - String value = (String) entry.getValue(); - if (value.contains(noteId)) { - edit.putString(entry.getKey(), data); - ids.add(entry.getKey()); - } + List ids = new ArrayList<>(); + + // Match on the note's id, not on the raw JSON containing it somewhere: a note whose body + // happens to mention another note's id is not the same note. + for (Map.Entry entry : WidgetUtils.getWidgetNotes(getReactApplicationContext()).entrySet()) { + if (!noteId.equals(entry.getValue().getId())) continue; + edit.putString(String.valueOf(entry.getKey()), data); + ids.add(entry.getKey()); } edit.apply(); - for (String id: ids) { - NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), Integer.parseInt(id)); + + for (int id : ids) { + NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), id); } } diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java index fa1d053a2..d3ab69a64 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java @@ -13,11 +13,14 @@ import android.widget.RemoteViews; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.streetwriters.notesnook.datatypes.Note; import com.streetwriters.notesnook.datatypes.Reminder; import java.util.ArrayList; import java.util.Calendar; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * Shared helpers for the home screen widgets. @@ -33,6 +36,50 @@ public class WidgetUtils { */ private static final int MAX_REMINDERS = 50; + /** + * The note each note widget is showing, keyed by widget id. + * + * The preferences file mixes two things: one note per widget id, and the reminders list under + * its own key. Only numeric keys are widget notes, so anything else is skipped rather than + * being treated as a note. + */ + static Map getWidgetNotes(Context context) { + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + Map notes = new LinkedHashMap<>(); + + for (Map.Entry entry : preferences.getAll().entrySet()) { + Integer widgetId = parseWidgetId(entry.getKey()); + if (widgetId == null) continue; + if (!(entry.getValue() instanceof String)) continue; + + Note note = parseNote((String) entry.getValue()); + if (note == null || note.getId() == null) continue; + notes.put(widgetId, note); + } + return notes; + } + + /** + * The widget id a preferences key refers to, or null if the key is not a widget id at all. + */ + private static Integer parseWidgetId(String key) { + try { + return Integer.valueOf(key); + } catch (NumberFormatException e) { + return null; + } + } + + static Note parseNote(String data) { + if (data == null || data.isEmpty()) return null; + try { + return new Gson().fromJson(data, Note.class); + } catch (Exception e) { + Log.e("NotePreviewWidget", "Could not read a stored note", e); + return null; + } + } + /** * The reminders the app last wrote out, minus any that have since fired. Reading and filtering * happens here so the provider can push the rows straight into the widget. From 1889b1345976fa0b566b7f657b2ab075009299ec Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:05:28 +0500 Subject: [PATCH 20/39] mobile: remove stray quote in NotePreviewWidget manifest entry --- apps/mobile/android/app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index fc7f88466..4b6724133 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -86,7 +86,7 @@ android:name=".NotePreviewWidget" android:exported="false" android:label="@string/note"> - " + From 70fd3096d2e269ba2d584e017028ff76cdacfa0a Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:10:25 +0500 Subject: [PATCH 21/39] mobile: fix invisible empty state in the reminders widget --- .../app/src/main/res/layout/widget_reminder_empty.xml | 8 -------- .../android/app/src/main/res/layout/widget_reminders.xml | 3 ++- apps/mobile/android/app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml deleted file mode 100644 index 9be87bb7a..000000000 --- a/apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml +++ /dev/null @@ -1,8 +0,0 @@ - \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml index f7f25bf55..c7bc790c5 100644 --- a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml +++ b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml @@ -57,7 +57,8 @@ android:layout_height="match_parent" android:textAlignment="center" android:gravity="center" - android:text="Tap on + to add reminder"/> + android:textColor="@color/text" + android:text="@string/widget_reminders_empty"/> diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index 8652b200d..51cd67101 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -9,6 +9,8 @@ Note Add a note to home screen Quick note + Tap + to add a reminder + Meeting notes Discuss the roadmap and agree on timelines. From 93337b6560741ed6d08b1fb4acf7adb2ca7d32e0 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:14:13 +0500 Subject: [PATCH 22/39] mobile: remove dead widget theme overlay, use system corner radius ThemeOverlay.Notesnook.AppWidgetContainer declared and set appWidgetBackgroundColor/appWidgetTextColor, but no layout ever read them - leftovers from the Android Studio widget template, in a palette unrelated to the app. Removes the overlay, its declare-styleable, the android:theme references, and the light_blue_* colors that only it used. Also replaces the hardcoded 10dp widget corner radius with system_app_widget_background_radius on API 31+ (10dp fallback below), so the widgets match the rest of the home screen. --- .../android/app/src/main/res/drawable/layout_bg.xml | 2 +- .../android/app/src/main/res/layout/new_note_widget.xml | 3 +-- .../android/app/src/main/res/layout/note_widget.xml | 3 +-- .../app/src/main/res/layout/note_widget_preview.xml | 3 +-- .../android/app/src/main/res/values-night/colors.xml | 4 ---- .../mobile/android/app/src/main/res/values-v31/dimens.xml | 8 ++++++++ apps/mobile/android/app/src/main/res/values/attrs.xml | 6 ------ apps/mobile/android/app/src/main/res/values/colors.xml | 4 ---- apps/mobile/android/app/src/main/res/values/dimens.xml | 4 ++++ apps/mobile/android/app/src/main/res/values/themes.xml | 7 ------- 10 files changed, 16 insertions(+), 28 deletions(-) create mode 100644 apps/mobile/android/app/src/main/res/values-v31/dimens.xml delete mode 100644 apps/mobile/android/app/src/main/res/values/attrs.xml delete mode 100644 apps/mobile/android/app/src/main/res/values/themes.xml diff --git a/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml b/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml index 4e9108ff5..af55a9847 100644 --- a/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml +++ b/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml b/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml index 622886bd1..70bb73b1b 100644 --- a/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml +++ b/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml @@ -2,8 +2,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@android:color/transparent" - android:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer"> + android:background="@android:color/transparent"> + android:background="@android:color/transparent"> + android:background="@android:color/transparent"> - #FFE1F5FE - #FF81D4FA - #FF039BE5 - #FF01579B #1f1f1f #1D1D1D #2E2E2E diff --git a/apps/mobile/android/app/src/main/res/values-v31/dimens.xml b/apps/mobile/android/app/src/main/res/values-v31/dimens.xml new file mode 100644 index 000000000..c3e87c9e4 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values-v31/dimens.xml @@ -0,0 +1,8 @@ + + + + + @android:dimen/system_app_widget_background_radius + + diff --git a/apps/mobile/android/app/src/main/res/values/attrs.xml b/apps/mobile/android/app/src/main/res/values/attrs.xml deleted file mode 100644 index 97531a256..000000000 --- a/apps/mobile/android/app/src/main/res/values/attrs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/colors.xml b/apps/mobile/android/app/src/main/res/values/colors.xml index 4ac8bc762..ab4d9ecc4 100644 --- a/apps/mobile/android/app/src/main/res/values/colors.xml +++ b/apps/mobile/android/app/src/main/res/values/colors.xml @@ -1,9 +1,5 @@ - #FFE1F5FE - #FF81D4FA - #FF039BE5 - #FF01579B #FFFFFF #DCEDEDED #BFBFBF diff --git a/apps/mobile/android/app/src/main/res/values/dimens.xml b/apps/mobile/android/app/src/main/res/values/dimens.xml index 4db8c5906..7aa2b16bc 100644 --- a/apps/mobile/android/app/src/main/res/values/dimens.xml +++ b/apps/mobile/android/app/src/main/res/values/dimens.xml @@ -7,4 +7,8 @@ http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout --> 0dp + + 10dp + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/themes.xml b/apps/mobile/android/app/src/main/res/values/themes.xml deleted file mode 100644 index 935088e01..000000000 --- a/apps/mobile/android/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file From 8d2e1c65bc456068b7234aa22f9eefa36da33bce Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:21:49 +0500 Subject: [PATCH 23/39] mobile: keep note and reminder content off the lock screen From Android 16 QPR1 widgets are lock screen eligible by default, so the note widget (title + preview text) and the reminders widget (titles and descriptions) could render user content on a locked device. Adds res/xml-v36 overrides declaring not_keyguard for both. The new note widget is left eligible - it is only a button and leaks nothing. --- .../src/main/res/xml-v36/note_widget_info.xml | 22 +++++++++++++++++++ .../res/xml-v36/widget_reminders_info.xml | 20 +++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml create mode 100644 apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml diff --git a/apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml b/apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml new file mode 100644 index 000000000..97a47ff1a --- /dev/null +++ b/apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml b/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml new file mode 100644 index 000000000..9bca769e1 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml @@ -0,0 +1,20 @@ + + \ No newline at end of file From b066fac6923374e325dadaf38d21f6cbec0f70fa Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:24:55 +0500 Subject: [PATCH 24/39] mobile: drop unused imports from widget source --- .../streetwriters/notesnook/NotePreviewConfigureActivity.java | 4 ---- .../java/com/streetwriters/notesnook/RCTNNativeModule.java | 1 - 2 files changed, 5 deletions(-) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java index 4418ce56f..3c6035abd 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java @@ -5,15 +5,11 @@ import android.appwidget.AppWidgetManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.util.Log; -import android.widget.RemoteViews; import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; import com.facebook.react.defaults.DefaultReactActivityDelegate; -import com.google.gson.Gson; -import com.streetwriters.notesnook.datatypes.Note; public class NotePreviewConfigureActivity extends ReactActivity { diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java index cf21ecbdf..175a06da6 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java @@ -12,7 +12,6 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.RectF; import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; From 77e56f9da1f7ec545279e12d6fc0ef6517d06c27 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 29 Jul 2026 09:08:03 +0500 Subject: [PATCH 25/39] mobile: fix widgets do not refresh after app data is cleared, require a restart. --- .../notesnook/RCTNNativeModule.java | 10 ++++ .../streetwriters/notesnook/WidgetUtils.java | 54 +++++++++++++++++++ apps/mobile/app/hooks/use-app-events.tsx | 6 +++ .../app/services/note-preview-widget.ts | 4 ++ apps/mobile/app/utils/notesnook-module.ts | 2 + 5 files changed, 76 insertions(+) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java index 175a06da6..ac04cebcb 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java @@ -193,6 +193,16 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { } } + /** + * Redraws every widget from scratch. Needed because the app can be stopped while its widgets + * stay on the home screen: clearing app data empties the store without the widgets ever being + * told, so they keep showing content that is gone until something forces a redraw. + */ + @ReactMethod + public void refreshWidgets() { + WidgetUtils.refreshAll(mContext); + } + @ReactMethod public void updateReminderWidget() { AppWidgetManager wm = AppWidgetManager.getInstance(mContext); diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java index d3ab69a64..c04ec351c 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java @@ -1,6 +1,8 @@ package com.streetwriters.notesnook; import android.app.ActivityOptions; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -18,9 +20,11 @@ import com.streetwriters.notesnook.datatypes.Reminder; import java.util.ArrayList; import java.util.Calendar; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Shared helpers for the home screen widgets. @@ -36,6 +40,56 @@ public class WidgetUtils { */ private static final int MAX_REMINDERS = 50; + /** + * Redraws every widget that currently exists, and drops stored notes for widgets that no + * longer do. + * + * Everything else keys off what we have stored, which is fine while the app is running but + * leaves widgets showing content that no longer exists once the store is emptied underneath + * them (clearing app data) or a widget is removed while the app is stopped (onDeleted never + * arrives). Starting from the widgets the system knows about, rather than from our own data, + * is what makes this self-correcting. + * + * NoteWidget is left alone deliberately: it is a static button with no stored state, and its + * layout depends on the size it was last given. + */ + static void refreshAll(Context context) { + AppWidgetManager manager = AppWidgetManager.getInstance(context); + + int[] noteWidgetIds = manager.getAppWidgetIds( + new ComponentName(context, NotePreviewWidget.class)); + removeOrphanedNotes(context, noteWidgetIds); + for (int appWidgetId : noteWidgetIds) { + NotePreviewWidget.updateAppWidget(context, manager, appWidgetId); + } + + for (int appWidgetId : manager.getAppWidgetIds( + new ComponentName(context, ReminderWidgetProvider.class))) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders); + ReminderWidgetProvider.updateAppWidget(context, manager, appWidgetId, views); + } + } + + /** + * Drops stored notes whose widget is gone, so the preferences file cannot grow forever. + */ + private static void removeOrphanedNotes(Context context, int[] liveWidgetIds) { + Set live = new HashSet<>(); + for (int appWidgetId : liveWidgetIds) live.add(String.valueOf(appWidgetId)); + + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor edit = preferences.edit(); + boolean changed = false; + + for (String key : preferences.getAll().keySet()) { + // Leave anything that is not a widget id alone, the reminders list included. + if (parseWidgetId(key) == null || live.contains(key)) continue; + edit.remove(key); + changed = true; + } + if (changed) edit.apply(); + } + /** * The note each note widget is showing, keyed by widget id. * diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index 84bbd2e22..52f02f226 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -81,6 +81,7 @@ import { setUpdateAvailableMessage } from "../services/message"; import Navigation from "../services/navigation"; +import { NotePreviewWidget } from "../services/note-preview-widget"; import Notifications from "../services/notifications"; import PremiumService from "../services/premium"; import SettingsService from "../services/settings"; @@ -574,6 +575,11 @@ export const useAppEvents = () => { useEffect(() => { if (isAppLoading) return; + // Widgets outlive the app process, so they can be left showing content the app no longer has + // (most obviously after the user clears app data). Nothing can run at that moment, so the + // first launch afterwards is the earliest chance to put them right. + NotePreviewWidget.updateNotes(); + let subscriptions: EventManagerSubscription[] = []; const eventManager = db.eventManager; subscriptions = [ diff --git a/apps/mobile/app/services/note-preview-widget.ts b/apps/mobile/app/services/note-preview-widget.ts index 8f7610732..cbb259e6a 100644 --- a/apps/mobile/app/services/note-preview-widget.ts +++ b/apps/mobile/app/services/note-preview-widget.ts @@ -34,6 +34,10 @@ export const NotePreviewWidget = { NotesnookModule.updateWidgetNote(id, JSON.stringify(newNote)); } + // Redraw from the widgets that actually exist rather than only the ones we + // have notes for. After app data is cleared there are none, and the widgets + // would otherwise keep showing content that no longer exists. + NotesnookModule.refreshWidgets(); }, 500); }, updateNote: async (id: string, note: Note) => { diff --git a/apps/mobile/app/utils/notesnook-module.ts b/apps/mobile/app/utils/notesnook-module.ts index d682e3f28..0d90c4681 100644 --- a/apps/mobile/app/utils/notesnook-module.ts +++ b/apps/mobile/app/utils/notesnook-module.ts @@ -45,6 +45,7 @@ interface NotesnookModuleInterface { hasWidgetNote: (noteId: string) => Promise; updateWidgetNote: (noteId: string, data: string) => void; updateReminderWidget: () => void; + refreshWidgets: () => void; isGestureNavigationEnabled: () => boolean; addShortcut: ( id: string, @@ -81,6 +82,7 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({ hasWidgetNote: () => {}, updateWidgetNote: () => {}, updateReminderWidget: () => {}, + refreshWidgets: () => {}, isGestureNavigationEnabled: () => true, addShortcut: () => Promise.resolve(false), removeShortcut: () => Promise.resolve(false), From 71cae07dc02c5bcb19e62500b36a92e0490e534e Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 29 Jul 2026 09:13:59 +0500 Subject: [PATCH 26/39] mobile: update widgets on time changed When system time changes, reminder widgets keep showing reminders that have passed as active. --- .../android/app/src/main/AndroidManifest.xml | 9 ++++++ .../notesnook/WidgetTimeChangeReceiver.java | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetTimeChangeReceiver.java diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 4b6724133..a3267c669 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -96,6 +96,15 @@ android:resource="@xml/note_widget_info" /> + + + + + + + Date: Wed, 29 Jul 2026 09:37:26 +0500 Subject: [PATCH 27/39] mobile: reminder widget does not show up on some devices --- .../src/main/res/xml-v36/widget_reminders_info.xml | 13 ++++--------- .../app/src/main/res/xml/widget_reminders_info.xml | 8 ++++---- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml b/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml index 9bca769e1..653f52e8f 100644 --- a/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml +++ b/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml @@ -1,14 +1,9 @@ - Date: Wed, 29 Jul 2026 10:01:29 +0500 Subject: [PATCH 28/39] mobile: fix reminder widget title vertical padding --- .../android/app/src/main/res/layout/widget_reminders.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml index c7bc790c5..c2b1683e2 100644 --- a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml +++ b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml @@ -12,22 +12,23 @@ android:orientation="horizontal" android:paddingHorizontal="12dp" android:layout_gravity="center" - android:paddingTop="12dp" + android:paddingTop="8dp" + android:paddingBottom="8dp" > From 7f5215403ba6f082704b9bf08ac7daa327b8543f Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 29 Jul 2026 12:14:15 +0500 Subject: [PATCH 29/39] mobile: keep reminders showing in widget after they go off for 3 hours --- .../notesnook/ReminderWidgetProvider.java | 2 +- .../streetwriters/notesnook/WidgetUtils.java | 29 ++++++++++----- apps/mobile/app/services/notifications.ts | 37 +++++++++++-------- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java index 4619f594c..bcbfb0233 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java @@ -43,7 +43,7 @@ public class ReminderWidgetProvider extends AppWidgetProvider { // The rows travel with the update itself, so there is no bound service to keep in sync and // nothing to invalidate separately: every update redraws from the current data. - List reminders = WidgetUtils.getActiveReminders(context); + List reminders = WidgetUtils.getWidgetReminders(context); RemoteViewsCompat.RemoteCollectionItems.Builder items = new RemoteViewsCompat.RemoteCollectionItems.Builder(); for (Reminder reminder : reminders) { diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java index c04ec351c..c23d55c44 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java @@ -25,6 +25,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; /** * Shared helpers for the home screen widgets. @@ -135,10 +136,10 @@ public class WidgetUtils { } /** - * The reminders the app last wrote out, minus any that have since fired. Reading and filtering - * happens here so the provider can push the rows straight into the widget. + * The reminders the app last wrote out, minus any that have now dropped out of view. Reading + * and filtering happens here so the provider can push the rows straight into the widget. */ - static List getActiveReminders(Context context) { + static List getWidgetReminders(Context context) { SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); List stored = null; try { @@ -152,7 +153,7 @@ public class WidgetUtils { if (stored == null) return active; for (Reminder reminder : stored) { - if (!isReminderActive(reminder)) continue; + if (!isVisibleInWidget(reminder)) continue; if (active.size() >= MAX_REMINDERS) { Log.w("Reminders", "Widget list truncated to " + MAX_REMINDERS + " reminders"); break; @@ -211,12 +212,20 @@ public class WidgetUtils { } /** - * Whether a reminder should still be listed. Mirrors isReminderActive() in the core package, - * which the app applies when it writes the list out. We re-check here because the stored list - * is only rewritten while the app runs, so one-off reminders would otherwise linger in the - * widget long after they fired. + * How long a reminder keeps its place in the list after going off, so the user can see that it + * happened rather than watching it vanish. Must match RECENTLY_PASSED_WINDOW in + * services/notifications.ts, which decides what gets written out in the first place. */ - static boolean isReminderActive(Reminder reminder) { + private static final long RECENTLY_PASSED_WINDOW_MS = TimeUnit.HOURS.toMillis(3); + + /** + * Whether a reminder should still be drawn. + * + * We re-check here rather than trusting the stored list because that list is only rewritten + * while the app runs. This is what actually retires a reminder once its grace period is up: + * every redraw re-evaluates it against the current time. + */ + static boolean isVisibleInWidget(Reminder reminder) { if (reminder == null) return false; if (reminder.isDisabled()) return false; @@ -225,7 +234,7 @@ public class WidgetUtils { if (!"once".equals(reminder.getMode())) return true; long triggerDate = reminder.getTriggerDate() > 0 ? reminder.getTriggerDate() : reminder.getDate(); - return triggerDate > now; + return triggerDate > now - RECENTLY_PASSED_WINDOW_MS; } /** diff --git a/apps/mobile/app/services/notifications.ts b/apps/mobile/app/services/notifications.ts index 389beca39..6dd620b5c 100644 --- a/apps/mobile/app/services/notifications.ts +++ b/apps/mobile/app/services/notifications.ts @@ -271,6 +271,8 @@ type ReminderWithFormattedTime = Reminder & { formattedDateTime?: string; }; +const RECENTLY_PASSED_WINDOW = 3 * 60 * 60 * 1000; + async function updateRemindersForWidget() { if (Platform.OS === "ios") return; const reminders: ReminderWithFormattedTime[] = await db.reminders?.all.items( @@ -280,28 +282,33 @@ async function updateRemindersForWidget() { sortDirection: "asc" } ); - const activeReminders = []; + const widgetReminders = []; if (!reminders) return; for (const reminder of reminders) { - if (isReminderActive(reminder)) { - const triggerDate = - reminder.snoozeUntil && reminder.snoozeUntil > Date.now() - ? reminder.snoozeUntil - : reminder.mode === "repeat" - ? getUpcomingReminderTime(reminder) - : reminder.date; + const triggerDate = + reminder.snoozeUntil && reminder.snoozeUntil > Date.now() + ? reminder.snoozeUntil + : reminder.mode === "repeat" + ? getUpcomingReminderTime(reminder) + : reminder.date; - reminder.triggerDate = triggerDate; - reminder.formattedTimeOfDay = getFormattedDate(triggerDate, "time"); - reminder.formattedDateTime = getFormattedDate(triggerDate, "date-time"); - reminder.formattedTime = getFormattedReminderTime(reminder); - activeReminders.push(reminder); - } + const recentlyPassed = + reminder.mode === "once" && + !reminder.disabled && + triggerDate > Date.now() - RECENTLY_PASSED_WINDOW; + + if (!isReminderActive(reminder) && !recentlyPassed) continue; + + reminder.triggerDate = triggerDate; + reminder.formattedTimeOfDay = getFormattedDate(triggerDate, "time"); + reminder.formattedDateTime = getFormattedDate(triggerDate, "date-time"); + reminder.formattedTime = getFormattedReminderTime(reminder); + widgetReminders.push(reminder); } NotesnookModule.setString( "appPreview", "remindersList", - JSON.stringify(activeReminders) + JSON.stringify(widgetReminders) ); NotesnookModule.updateReminderWidget(); } From eef56916dee9c0d2ab63a0a0e232406575d042b1 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 30 Jul 2026 09:40:43 +0500 Subject: [PATCH 30/39] mobile: Upcoming Reminders -> Reminders in home widgets --- .../mobile/android/app/src/main/res/layout/widget_reminders.xml | 2 +- .../app/src/main/res/layout/widget_reminders_preview.xml | 2 +- apps/mobile/android/app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml index c2b1683e2..10b723840 100644 --- a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml +++ b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml @@ -23,7 +23,7 @@ android:textSize="15sp" android:textStyle="bold" android:textColor="@color/text" - android:text="Upcoming Reminders"/> + android:text="Reminders"/> diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index 51cd67101..d5b719bcd 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -4,7 +4,7 @@ EXAMPLE Add widget Take a quick note. - Quick overview of upcoming reminders + Quick overview of reminders Reminders Note Add a note to home screen From 4765657423da9e540573ef9a7735f2e2883fb128 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com> Date: Thu, 30 Jul 2026 22:55:06 +0500 Subject: [PATCH 31/39] mobile: fix editor cross contamination (#10157) 1. Use noteId defined in editorMessage when saving content, never use tabId since it can point to a different note since a tab can load a different note while a note save message is coming across the bridge. 2. If saving fails and editor saves save payloads in localStorage, add the noteId to them so we know which note the content belongs to. 3. Pending content saves must keep noteId and edit time so when we save them later, they save into the correct note and if the content is newer, we skip saving. 4. Fix the debounce key so cross contamination can never occur in new notes between two tabs. --- .../mobile/app/screens/editor/tiptap/types.ts | 2 + .../editor/tiptap/use-editor-events.tsx | 14 ++- .../app/screens/editor/tiptap/use-editor.ts | 116 ++++++++++++++---- .../src/hooks/useEditorController.ts | 47 ++++--- .../editor-mobile/src/utils/pending-saves.ts | 16 ++- 5 files changed, 141 insertions(+), 54 deletions(-) diff --git a/apps/mobile/app/screens/editor/tiptap/types.ts b/apps/mobile/app/screens/editor/tiptap/types.ts index 803020a79..6778cc7b8 100644 --- a/apps/mobile/app/screens/editor/tiptap/types.ts +++ b/apps/mobile/app/screens/editor/tiptap/types.ts @@ -93,6 +93,8 @@ export type SavePayload = { ignoreEdit: boolean; tabId: string; pendingChanges?: boolean; + sourceNoteId?: string; + pendingChangesAt?: number; }; export type AppState = { 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 276d2c0a5..716963271 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx +++ b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx @@ -411,16 +411,20 @@ export const useEditorEvents = ( .getState() .getNoteIdForTab(editorMessage.tabId); + const saveNoteId = editorMessage.noteId || noteId; + switch (editorMessage.type) { case EditorEvents.content: DatabaseLogger.log("EditorEvents.content"); editor.saveContent({ type: editorMessage.type, content: editorMessage.value.html as string, - noteId: noteId, + noteId: saveNoteId, + sourceNoteId: editorMessage.noteId, tabId: editorMessage.tabId, ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit, - pendingChanges: editorMessage.value?.pendingChanges + pendingChanges: editorMessage.value?.pendingChanges, + pendingChangesAt: editorMessage.value?.pendingChangesAt }); break; case EditorEvents.title: @@ -428,10 +432,12 @@ export const useEditorEvents = ( editor.saveContent({ type: editorMessage.type, title: editorMessage.value?.title as string, - noteId: noteId, + noteId: saveNoteId, + sourceNoteId: editorMessage.noteId, tabId: editorMessage.tabId, ignoreEdit: false, - pendingChanges: editorMessage.value?.pendingChanges + pendingChanges: editorMessage.value?.pendingChanges, + pendingChangesAt: editorMessage.value?.pendingChangesAt }); break; case EditorEvents.logger: diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor.ts b/apps/mobile/app/screens/editor/tiptap/use-editor.ts index 830331751..3e24ee06a 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-editor.ts @@ -263,26 +263,63 @@ export const useEditor = ( ignoreEdit, sessionHistoryId: currentSessionHistoryId, tabId, - pendingChanges + pendingChanges, + sourceNoteId, + pendingChangesAt }: SavePayload) => { if (currentNotes.current[id as string]?.readonly || readonly) return; + + if (sourceNoteId && id && sourceNoteId !== id) { + DatabaseLogger.error( + new Error( + `Refused to save content of note ${sourceNoteId} into note ${id}` + ) + ); + return; + } + try { if (id && !(await db.notes?.note(id))) { - await reset(tabId); - useTabStore.getState().updateTab(tabId, { - session: { - noteId: undefined, - noteLocked: undefined, - locked: undefined, - readonly: undefined, - scrollTop: undefined, - selection: undefined, - spellCheckDisabled: false - } - }); + if (useTabStore.getState().getNoteIdForTab(tabId) === id) { + await reset(tabId); + useTabStore.getState().updateTab(tabId, { + session: { + noteId: undefined, + noteLocked: undefined, + locked: undefined, + readonly: undefined, + scrollTop: undefined, + selection: undefined, + spellCheckDisabled: false + } + }); + } return; } let note = id ? await db.notes?.note(id) : undefined; + + // A restored pending change can be older than what is already in the + // db (it was saved on another device, or the save actually went + // through and only the acknowledgement was lost). Applying it would + // roll the note back, so verify it is still the newest edit. Content + // and title are compared separately so that a newer title doesn't + // discard pending content, and vice versa. + if (pendingChanges && pendingChangesAt && note) { + const dateEdited = data + ? note.contentId + ? (await db.content?.get(note.contentId))?.dateEdited + : undefined + : note.dateEdited; + + if (dateEdited && dateEdited > pendingChangesAt) { + DatabaseLogger.log( + `Discarding stale pending ${ + data ? "content" : "title" + } for note ${id}: edited at ${dateEdited}, change captured at ${pendingChangesAt}` + ); + return id; + } + } const locked = note && (await db.vaults.itemExists(note)); if (note?.conflicted) { @@ -321,6 +358,9 @@ export const useEditor = ( let saved = false; setTimeout(() => { if (saved) return; + // Don't report progress on a tab that has moved on to another note. + if (id && useTabStore.getState().getNoteIdForTab(tabId) !== id) + return; commands.setStatus( getFormattedDate(note ? note.dateEdited : Date.now(), "date-time"), strings.saving(), @@ -436,14 +476,26 @@ export const useEditor = ( } } - if ( - id && - id === useTabStore.getState().getCurrentNoteId() && - pendingChanges - ) { - postMessage(NativeEvents.title, title || note?.title, tabId); - postMessage(NativeEvents.html, data, tabId); - currentNotes.current[id] = note; + if (id && pendingChanges) { + if (data) { + currentContents.current[id] = { + data: data, + type: "tiptap", + noteId: id + }; + } + lastContentChangeTime.current[id] = Date.now(); + + // Push the restored change into the editor only if the note is + // actually open in a tab, and only into that tab. + const noteTabId = useTabStore.getState().getTabForNote(id); + if (noteTabId !== undefined) { + postMessage(NativeEvents.title, title || note?.title, noteTabId); + if (data) { + postMessage(NativeEvents.html, { data: data }, noteTabId); + } + currentNotes.current[id] = note; + } } if (!saveCount.current[tabId]) { @@ -935,7 +987,9 @@ export const useEditor = ( ignoreEdit, noteId, tabId, - pendingChanges + pendingChanges, + sourceNoteId, + pendingChangesAt }: { noteId?: string; title?: string; @@ -944,6 +998,8 @@ export const useEditor = ( ignoreEdit: boolean; tabId: string; pendingChanges?: boolean; + sourceNoteId?: string; + pendingChangesAt?: number; }) => { DatabaseLogger.log( `saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}` @@ -971,7 +1027,10 @@ export const useEditor = ( return; } - if (noteId) { + // A restored pending change is not a live edit: it may still be + // discarded as stale by saveNote, so it must not claim to be the newest + // content until it is actually written. + if (noteId && !pendingChanges) { lastContentChangeTime.current[noteId] = Date.now(); localTabState.current?.setEditTime(noteId, Date.now()); localTabState?.current?.set(tabId, { @@ -979,7 +1038,7 @@ export const useEditor = ( }); } - if (type === EditorEvents.content && noteId) { + if (type === EditorEvents.content && noteId && !pendingChanges) { currentContents.current[noteId as string] = { data: content, type: "tiptap", @@ -995,12 +1054,15 @@ export const useEditor = ( ignoreEdit, sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined, tabId: tabId, - pendingChanges + pendingChanges, + sourceNoteId, + pendingChangesAt }; + withTimer( - noteId || "newnote", + `${noteId || tabId}:${type}`, () => { - if (!params.id) { + if (!params.id && !params.sourceNoteId) { params.id = useTabStore.getState().getNoteIdForTab(tabId); } if (onChange && params.data) { diff --git a/packages/editor-mobile/src/hooks/useEditorController.ts b/packages/editor-mobile/src/hooks/useEditorController.ts index 169b5d6fc..5755b94da 100644 --- a/packages/editor-mobile/src/hooks/useEditorController.ts +++ b/packages/editor-mobile/src/hooks/useEditorController.ts @@ -153,18 +153,17 @@ export function useEditorController({ const titleChange = useCallback(async (title: string) => { if (!isReactNative()) return; const currentSessionId = globalThis.sessionId; - post( - EditorEvents.contentchange, - undefined, - tabRef.current.id, - tabRef.current.session?.noteId - ); + const editedAt = Date.now(); + + const tabId = tabRef.current.id; + const noteId = tabRef.current.session?.noteId; + post(EditorEvents.contentchange, undefined, tabId, noteId); const params = [ { title }, - tabRef.current.id, - tabRef.current.session?.noteId, + tabId, + noteId, currentSessionId, 1000 ]; @@ -186,12 +185,12 @@ export function useEditorController({ `Saving title failed, setting pending request ${pendingTitleIds.length}` ); if (params[2]) { - pendingSaveRequests.setTitle(params); + pendingSaveRequests.setTitle(params, editedAt); } const element = document.getElementById("editor-saving-failed-overlay"); if (element) { element.style.display = "flex"; - editors[tabRef.current.id]?.commands?.blur(); + editors[tabId]?.commands?.blur(); element.focus(); } }); @@ -217,26 +216,36 @@ export function useEditorController({ return; } const currentSessionId = globalThis.sessionId; - post( - EditorEvents.contentchange, - undefined, - tabRef.current.id, - tabRef.current.session?.noteId - ); + const tabId = tabRef.current.id; + const noteId = tabRef.current.session?.noteId; + post(EditorEvents.contentchange, undefined, tabId, noteId); if (!editor) return; if (typeof timers.current.change === "number") { clearTimeout(timers.current?.change); } timers.current.change = setTimeout(async () => { + if (tabRef.current.session?.noteId !== noteId) { + logger( + "info", + `Edit discarded, tab ${tabId} moved from note ${noteId} to ${tabRef.current.session?.noteId}` + ); + return; + } + if (editorControllers[tabId]?.loading) { + logger("info", "Edit discarded, tab is in loading state"); + return; + } + + const editedAt = Date.now(); htmlContentRef.current = editor.getHTML(); const params = [ { html: htmlContentRef.current, ignoreEdit: ignoreEdit }, - tabRef.current.id, - tabRef.current.session?.noteId, + tabId, + noteId, currentSessionId, 5000 ]; @@ -262,7 +271,7 @@ export function useEditorController({ }` ); if (params[2]) { - pendingSaveRequests.setContent(params); + pendingSaveRequests.setContent(params, editedAt); } const element = document.getElementById( diff --git a/packages/editor-mobile/src/utils/pending-saves.ts b/packages/editor-mobile/src/utils/pending-saves.ts index b81703aa1..71c7a9fb4 100644 --- a/packages/editor-mobile/src/utils/pending-saves.ts +++ b/packages/editor-mobile/src/utils/pending-saves.ts @@ -23,13 +23,14 @@ class PendingSaveRequests { static TITLES = "pendingTitles"; static CONTENT = "pendingContents"; - async setTitle(value: any) { + async setTitle(value: any, editedAt: number) { const pendingTitles = JSON.parse( this.get(PendingSaveRequests.TITLES) || "[]" ); (pendingTitles as any[]).push({ id: randId("title-pending"), + editedAt, params: value }); return localStorage.setItem( @@ -45,13 +46,14 @@ class PendingSaveRequests { return pendingTitles; } - async setContent(value: any) { + async setContent(value: any, editedAt: number) { const pendingContents = JSON.parse( this.get(PendingSaveRequests.CONTENT) || "[]" ); (pendingContents as any[]).push({ id: randId("content-pending"), + editedAt, params: value }); return localStorage.setItem( @@ -118,7 +120,10 @@ class PendingSaveRequests { const pendingTitles = await this.getPendingTitles(); this.remove(PendingSaveRequests.TITLES); for (const pending of pendingTitles) { - if (pending.params[0]) pending.params[0].pendingChanges = true; + if (pending.params[0]) { + pending.params[0].pendingChanges = true; + pending.params[0].pendingChangesAt = pending.editedAt; + } await postAsyncWithTimeout(EditorEvents.title, ...pending.params); } }; @@ -127,7 +132,10 @@ class PendingSaveRequests { const pendingContents = await this.getPendingContent(); this.remove(PendingSaveRequests.CONTENT); for (const pending of pendingContents) { - if (pending.params[0]) pending.params[0].pendingChanges = true; + if (pending.params[0]) { + pending.params[0].pendingChanges = true; + pending.params[0].pendingChangesAt = pending.editedAt; + } await postAsyncWithTimeout(EditorEvents.content, ...pending.params); } }; From a1879dae1751227953812ac9801668a0ec2d741d Mon Sep 17 00:00:00 2001 From: Suyadi <104284194+afsuyadi@users.noreply.github.com> Date: Fri, 31 Jul 2026 01:01:21 +0700 Subject: [PATCH 32/39] mobile: fix error on android's search query (#10106) * mobile: pass value for items when searching on main screen to match other screens' pattern Signed-off-by: Suyadi * core: guard notesWithHighlighting against an undefined notes selector. Signed-off-by: Suyadi * core: add regression test for notesWithHighlighting crash. Signed-off-by: Suyadi * core: revert changes after feedback Signed-off-by: Suyadi * mobile: ensure type: "note" will make items become mandatory. Signed-off-by: Suyadi * core: remove tests after feedback Signed-off-by: Suyadi --- apps/mobile/app/screens/home/index.tsx | 4 +++- .../mobile/app/stores/use-navigation-store.ts | 22 +++++++++++++------ packages/core/src/api/lookup.ts | 1 - 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/mobile/app/screens/home/index.tsx b/apps/mobile/app/screens/home/index.tsx index 46c4fa111..bdb556c49 100755 --- a/apps/mobile/app/screens/home/index.tsx +++ b/apps/mobile/app/screens/home/index.tsx @@ -30,6 +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 { db } from "../../common/database"; export const Home = ({ navigation, route }: NavigationProps<"Notes">) => { const [notes, loading] = useNotes(); @@ -59,7 +60,8 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => { placeholder: strings.searchInRoute(route.name), type: "note", title: route.name, - route: route.name + route: route.name, + items: db.notes.all }); }} id={route.name} diff --git a/apps/mobile/app/stores/use-navigation-store.ts b/apps/mobile/app/stores/use-navigation-store.ts index e3b1ff5c6..2d98a26ff 100644 --- a/apps/mobile/app/stores/use-navigation-store.ts +++ b/apps/mobile/app/stores/use-navigation-store.ts @@ -74,13 +74,21 @@ export interface RouteParams extends ParamListBase { Tags: GenericRouteParam; Favorites: GenericRouteParam; Trash: GenericRouteParam; - Search: { - placeholder: string; - type: ItemType; - title: string; - route: RouteName; - items?: FilteredSelector; - }; + Search: + | { + placeholder: string; + type: "note"; + title: string; + route: RouteName; + items: FilteredSelector; + } + | { + placeholder: string; + type: Exclude; + title: string; + route: RouteName; + items?: FilteredSelector; + }; TaggedNotes: NotesScreenParams; ColoredNotes: NotesScreenParams; TopicNotes: NotesScreenParams; diff --git a/packages/core/src/api/lookup.ts b/packages/core/src/api/lookup.ts index d1ae43f00..2096d10b8 100644 --- a/packages/core/src/api/lookup.ts +++ b/packages/core/src/api/lookup.ts @@ -114,7 +114,6 @@ export default class Lookup { ): Promise> { const db = this.db.sql() as unknown as Kysely; const excludedIds = this.db.trash.cache.notes; - const { content, title, From 41cdc882c9327c256393575fe7042c8248d89978 Mon Sep 17 00:00:00 2001 From: kashaf-ansari-dev Date: Tue, 28 Jul 2026 15:29:50 +0500 Subject: [PATCH 33/39] editor: fix ignoreEdit flag overwritten during debounced save Signed-off-by: kashaf-ansari-dev --- .../src/hooks/useEditorController.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/editor-mobile/src/hooks/useEditorController.ts b/packages/editor-mobile/src/hooks/useEditorController.ts index 5755b94da..4d7f8008b 100644 --- a/packages/editor-mobile/src/hooks/useEditorController.ts +++ b/packages/editor-mobile/src/hooks/useEditorController.ts @@ -215,13 +215,20 @@ export function useEditorController({ logger("info", "Edit skipped, tab is in loading state"); return; } + + const timerPending = !!timers.current.change; + if (ignoreEdit && timerPending) { + logger("info", "Ignoring ignoreEdit update, a save is already pending"); + return; + } + const currentSessionId = globalThis.sessionId; const tabId = tabRef.current.id; const noteId = tabRef.current.session?.noteId; post(EditorEvents.contentchange, undefined, tabId, noteId); if (!editor) return; - if (typeof timers.current.change === "number") { - clearTimeout(timers.current?.change); + if (timerPending) { + clearTimeout(timers.current?.change as any); } timers.current.change = setTimeout(async () => { @@ -239,6 +246,8 @@ export function useEditorController({ const editedAt = Date.now(); htmlContentRef.current = editor.getHTML(); + timers.current.change = null; + const params = [ { html: htmlContentRef.current, @@ -284,7 +293,7 @@ export function useEditorController({ }); logger("info", "Editor saving content", params[1], params[2]); - }, 300); + }, 100); countWords(5000); }, From 02c60d14c7653c8ba266e68ea34154943ef3b388 Mon Sep 17 00:00:00 2001 From: kashaf-ansari-dev Date: Wed, 29 Jul 2026 10:31:36 +0500 Subject: [PATCH 34/39] editor: skip ignoreEdit updates before scheduling save Signed-off-by: kashaf-ansari-dev --- packages/editor-mobile/src/hooks/useEditorController.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/editor-mobile/src/hooks/useEditorController.ts b/packages/editor-mobile/src/hooks/useEditorController.ts index 4d7f8008b..e5b9a2ceb 100644 --- a/packages/editor-mobile/src/hooks/useEditorController.ts +++ b/packages/editor-mobile/src/hooks/useEditorController.ts @@ -216,8 +216,7 @@ export function useEditorController({ return; } - const timerPending = !!timers.current.change; - if (ignoreEdit && timerPending) { + if (ignoreEdit) { logger("info", "Ignoring ignoreEdit update, a save is already pending"); return; } @@ -227,8 +226,8 @@ export function useEditorController({ const noteId = tabRef.current.session?.noteId; post(EditorEvents.contentchange, undefined, tabId, noteId); if (!editor) return; - if (timerPending) { - clearTimeout(timers.current?.change as any); + if (typeof timers.current.change === "number") { + clearTimeout(timers.current?.change); } timers.current.change = setTimeout(async () => { @@ -246,7 +245,6 @@ export function useEditorController({ const editedAt = Date.now(); htmlContentRef.current = editor.getHTML(); - timers.current.change = null; const params = [ { From 28f2ffdbf707f7bf7b5357ab2b4beb779042df35 Mon Sep 17 00:00:00 2001 From: kashaf-ansari-dev Date: Wed, 29 Jul 2026 10:32:47 +0500 Subject: [PATCH 35/39] mobile: remove unused ignoreEdit handling Signed-off-by: kashaf-ansari-dev --- apps/mobile/app/screens/editor/tiptap/types.ts | 1 - .../app/screens/editor/tiptap/use-editor-events.tsx | 4 +--- apps/mobile/app/screens/editor/tiptap/use-editor.ts | 13 +------------ 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/apps/mobile/app/screens/editor/tiptap/types.ts b/apps/mobile/app/screens/editor/tiptap/types.ts index 6778cc7b8..ca96340c9 100644 --- a/apps/mobile/app/screens/editor/tiptap/types.ts +++ b/apps/mobile/app/screens/editor/tiptap/types.ts @@ -90,7 +90,6 @@ export type SavePayload = { data?: string; type?: "tiptap"; sessionHistoryId?: number; - ignoreEdit: boolean; tabId: string; pendingChanges?: boolean; sourceNoteId?: string; 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 716963271..e1c8cac84 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx +++ b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx @@ -149,7 +149,7 @@ const showActionsheet = async () => { } }; -type ContentMessage = { html: string; ignoreEdit: boolean }; +type ContentMessage = { html: string }; export const useEditorEvents = ( editor: useEditorType, @@ -422,7 +422,6 @@ export const useEditorEvents = ( noteId: saveNoteId, sourceNoteId: editorMessage.noteId, tabId: editorMessage.tabId, - ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit, pendingChanges: editorMessage.value?.pendingChanges, pendingChangesAt: editorMessage.value?.pendingChangesAt }); @@ -435,7 +434,6 @@ export const useEditorEvents = ( noteId: saveNoteId, sourceNoteId: editorMessage.noteId, tabId: editorMessage.tabId, - ignoreEdit: false, pendingChanges: editorMessage.value?.pendingChanges, pendingChangesAt: editorMessage.value?.pendingChangesAt }); diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor.ts b/apps/mobile/app/screens/editor/tiptap/use-editor.ts index 3e24ee06a..53776bbca 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-editor.ts @@ -260,7 +260,6 @@ export const useEditor = ( id, data, type, - ignoreEdit, sessionHistoryId: currentSessionHistoryId, tabId, pendingChanges, @@ -343,11 +342,6 @@ export const useEditor = ( noteData.title = title; - if (ignoreEdit) { - DatabaseLogger.log("Ignoring edits..."); - noteData.dateEdited = note?.dateEdited; - } - if (data) { noteData.content = { data: data, @@ -984,7 +978,6 @@ export const useEditor = ( title, content, type, - ignoreEdit, noteId, tabId, pendingChanges, @@ -995,7 +988,6 @@ export const useEditor = ( title?: string; content?: string; type: string; - ignoreEdit: boolean; tabId: string; pendingChanges?: boolean; sourceNoteId?: string; @@ -1005,7 +997,6 @@ export const useEditor = ( `saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}` ); if ( - ignoreEdit || lock.current || (currentLoadingNoteId.current && currentLoadingNoteId.current === noteId) @@ -1014,7 +1005,6 @@ export const useEditor = ( lock.current: ${lock.current} currentLoadingNoteId.current: ${currentLoadingNoteId.current} - ignoreEdit: ${ignoreEdit} `); if (lock.current) { setTimeout(() => { @@ -1051,7 +1041,6 @@ export const useEditor = ( data: content, type: "tiptap", id: noteId, - ignoreEdit, sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined, tabId: tabId, pendingChanges, @@ -1080,7 +1069,7 @@ export const useEditor = ( saveNote(params); } }, - ignoreEdit ? 0 : 150 + 150 ); }, [editorSessionHistory, withTimer, onChange, saveNote] From 57113e089641c4c55425a7311536b1ca5ee18f11 Mon Sep 17 00:00:00 2001 From: kashaf-ansari-dev Date: Fri, 31 Jul 2026 09:49:36 +0500 Subject: [PATCH 36/39] mobile: remove AddReminder from stack on navigation away to prevent stale form state Signed-off-by: kashaf-ansari-dev --- apps/mobile/app/screens/add-reminder/index.tsx | 2 +- apps/mobile/app/services/navigation.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/mobile/app/screens/add-reminder/index.tsx b/apps/mobile/app/screens/add-reminder/index.tsx index e6cc813fa..4315c047e 100644 --- a/apps/mobile/app/screens/add-reminder/index.tsx +++ b/apps/mobile/app/screens/add-reminder/index.tsx @@ -288,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); } diff --git a/apps/mobile/app/services/navigation.ts b/apps/mobile/app/services/navigation.ts index f3b4114d0..4bcc58eb9 100755 --- a/apps/mobile/app/services/navigation.ts +++ b/apps/mobile/app/services/navigation.ts @@ -177,9 +177,11 @@ function resetRootState( if (state.routes.length < 2) return; - const routes = state.routes.filter( + let routes = state.routes.filter( (route) => - (route.name !== "Auth" && route.name !== "Welcome") || + (route.name !== "Auth" && + route.name !== "Welcome" && + route.name !== "AddReminder") || route.key === focusedRoute.key ); From f858e3471f48d4e3b47c631cb2c723fe2a7a580c Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Fri, 31 Jul 2026 12:41:56 +0500 Subject: [PATCH 37/39] desktop: set snap base to core24 (#10180) --- apps/desktop/electron-builder.config.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/desktop/electron-builder.config.js b/apps/desktop/electron-builder.config.js index 1c8c0caa2..a13e962fc 100644 --- a/apps/desktop/electron-builder.config.js +++ b/apps/desktop/electron-builder.config.js @@ -203,11 +203,12 @@ module.exports = { toolsets: { appimage: "1.0.2" }, - snap: { - autoStart: false, - confinement: "strict", - allowNativeWayland: true, - base: "core22" + snapcraft: { + base: "core24", + core24: { + confinement: "strict", + autoStart: false + } }, extraResources: ["app-update.yml", "./assets/**"], extraMetadata: { From 427dcbbb20f70b146c3825aed96c078b28946bb8 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 3 Aug 2026 12:37:13 +0500 Subject: [PATCH 38/39] mobile: release v3.4.8 --- apps/mobile/android/app/build.gradle | 2 +- apps/mobile/android/releasenotes/whatsnew-en-US | 4 +++- apps/mobile/ios/build-configs/ios-build.active.xcconfig | 4 ++-- apps/mobile/ios/build-configs/ios-build.production.xcconfig | 4 ++-- apps/mobile/ios/build-configs/ios-build.staging.xcconfig | 4 ++-- apps/mobile/package.json | 2 +- fastlane/metadata/android/en-US/changelogs/15574.txt | 6 ++++++ 7 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/15574.txt diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 1f4781422..f90b30f98 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -140,7 +140,7 @@ android { if (project.hasProperty("prBuildNumber")) { versionCode Integer.parseInt(prBuildNumber()) } else { - versionCode 3113 + versionCode 3114 } versionName getNpmVersion() testBuildType System.getProperty('testBuildType', 'debug') diff --git a/apps/mobile/android/releasenotes/whatsnew-en-US b/apps/mobile/android/releasenotes/whatsnew-en-US index 0454f523e..35eb621d9 100644 --- a/apps/mobile/android/releasenotes/whatsnew-en-US +++ b/apps/mobile/android/releasenotes/whatsnew-en-US @@ -1,4 +1,6 @@ -- Add option to clear note version history +- Added sync status icon in sidebar +- Added new reminder shortcut in app icon context menu +- Improved editor saving reliability - Minor bug fixes and improvements Thank you for using Notesnook! diff --git a/apps/mobile/ios/build-configs/ios-build.active.xcconfig b/apps/mobile/ios/build-configs/ios-build.active.xcconfig index 4d0ea7252..e03bda205 100644 --- a/apps/mobile/ios/build-configs/ios-build.active.xcconfig +++ b/apps/mobile/ios/build-configs/ios-build.active.xcconfig @@ -1,6 +1,6 @@ // Production iOS build identifiers -IOS_CURRENT_PROJECT_VERSION = 2191 -IOS_MARKETING_VERSION = 3.4.7 +IOS_CURRENT_PROJECT_VERSION = 2192 +IOS_MARKETING_VERSION = 3.4.8 IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share diff --git a/apps/mobile/ios/build-configs/ios-build.production.xcconfig b/apps/mobile/ios/build-configs/ios-build.production.xcconfig index 4d0ea7252..e03bda205 100644 --- a/apps/mobile/ios/build-configs/ios-build.production.xcconfig +++ b/apps/mobile/ios/build-configs/ios-build.production.xcconfig @@ -1,6 +1,6 @@ // Production iOS build identifiers -IOS_CURRENT_PROJECT_VERSION = 2191 -IOS_MARKETING_VERSION = 3.4.7 +IOS_CURRENT_PROJECT_VERSION = 2192 +IOS_MARKETING_VERSION = 3.4.8 IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share diff --git a/apps/mobile/ios/build-configs/ios-build.staging.xcconfig b/apps/mobile/ios/build-configs/ios-build.staging.xcconfig index 2404c2515..b2efe4bf3 100644 --- a/apps/mobile/ios/build-configs/ios-build.staging.xcconfig +++ b/apps/mobile/ios/build-configs/ios-build.staging.xcconfig @@ -1,6 +1,6 @@ // Staging iOS build identifiers -IOS_CURRENT_PROJECT_VERSION = 2191 -IOS_MARKETING_VERSION = 3.4.7 +IOS_CURRENT_PROJECT_VERSION = 2192 +IOS_MARKETING_VERSION = 3.4.8 IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 50b7339e0..b5fd34034 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@notesnook/mobile", - "version": "3.4.7", + "version": "3.4.8", "private": true, "license": "GPL-3.0-or-later", "scripts": { diff --git a/fastlane/metadata/android/en-US/changelogs/15574.txt b/fastlane/metadata/android/en-US/changelogs/15574.txt new file mode 100644 index 000000000..35eb621d9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/15574.txt @@ -0,0 +1,6 @@ +- Added sync status icon in sidebar +- Added new reminder shortcut in app icon context menu +- Improved editor saving reliability +- Minor bug fixes and improvements + +Thank you for using Notesnook! From 31dbbe5e9e772721fc6c5fb2e60cad7935f58932 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Tue, 4 Aug 2026 12:11:54 +0500 Subject: [PATCH 39/39] mobile: fix rn 82 build on xcode 26.5 --- apps/mobile/ios/Podfile | 6 ++ apps/mobile/ios/Podfile.lock | 2 +- .../mobile/ios/scripts/patch_fmt_consteval.rb | 77 +++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 apps/mobile/ios/scripts/patch_fmt_consteval.rb diff --git a/apps/mobile/ios/Podfile b/apps/mobile/ios/Podfile index 5e28ff61a..a1cdf9023 100644 --- a/apps/mobile/ios/Podfile +++ b/apps/mobile/ios/Podfile @@ -5,6 +5,8 @@ require Pod::Executable.execute_command('node', ['-p', {paths: [process.argv[1]]}, )', __dir__]).strip +require_relative 'scripts/patch_fmt_consteval' + platform :ios, min_ios_version_supported prepare_react_native_project! @@ -66,6 +68,10 @@ post_install do |installer| :mac_catalyst_enabled => false, # :ccache_enabled => true ) + + # Keep fmt buildable on Xcode >= 26.2. See ios/scripts/patch_fmt_consteval.rb. + PatchFmtConsteval.apply!(installer.sandbox.root) + installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO' diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index 177862349..d576a454d 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -4092,6 +4092,6 @@ SPEC CHECKSUMS: toolbar-android: c426ed5bd3dcccfed20fd79533efc0d1ae0ef018 Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb -PODFILE CHECKSUM: 3fe13efa8356dcc061862bfa9f453dcd12ede70a +PODFILE CHECKSUM: 30b2045c0f4fc91402a43a9e2a872af803f2d6c3 COCOAPODS: 1.16.2 diff --git a/apps/mobile/ios/scripts/patch_fmt_consteval.rb b/apps/mobile/ios/scripts/patch_fmt_consteval.rb new file mode 100644 index 000000000..eb28baf08 --- /dev/null +++ b/apps/mobile/ios/scripts/patch_fmt_consteval.rb @@ -0,0 +1,77 @@ +# Xcode >= 26.2 rejects fmt's compile-time format-string check with +# +# call to consteval function 'fmt::fstring<...>::fstring' is not a +# constant expression +# +# React Native 0.81/0.82 vendor fmt 11.0.2, which hits this. It is an upstream +# incompatibility (reproducible in a stock RN app), but left alone it makes those +# RN versions permanently un-buildable on a modern Xcode. +# +# fmt's own escape hatch is FMT_USE_CONSTEVAL: 0 downgrades the format-string +# check from compile-time to run-time. fmt 11.0.2 does not guard its detection +# block with #ifndef, so we cannot simply predefine the macro -- and doing it +# through the build settings is worse anyway: +# +# * a command-line GCC_PREPROCESSOR_DEFINITIONS outranks every per-target +# value, silently dropping COCOAPODS=1, RCT_METRO_PORT, ... +# * a second `post_install` block in the Podfile REPLACES React Native's own. +# +# So we patch the header itself, right after fmt has made up its mind and before +# the first use of the macro. Idempotent, and safe to run on every pod install. + +module PatchFmtConsteval + MARKER = 'NOTESNOOK_FMT_CONSTEVAL_PATCH'.freeze + + # The line that first consumes the macro; our override goes immediately above + # it, i.e. after the whole detection cascade. + ANCHOR = "#if FMT_USE_CONSTEVAL\n".freeze + + OVERRIDE = <<~PATCH.freeze + // #{MARKER}: Xcode >= 26.2 rejects fmt's consteval format-string check + // (see ios/scripts/patch_fmt_consteval.rb). Applied automatically by + // `pod install`; downgrades the check to run-time. + #undef FMT_USE_CONSTEVAL + #define FMT_USE_CONSTEVAL 0 + PATCH + + # pods_root: the Pods directory (installer.sandbox.root). + def self.apply!(pods_root) + header = File.join(pods_root.to_s, 'fmt', 'include', 'fmt', 'base.h') + + unless File.exist?(header) + Pod::UI.warn "fmt: #{header} not found, skipping consteval patch." + return + end + + contents = File.read(header) + + if contents.include?(MARKER) + Pod::UI.puts 'fmt: consteval patch already applied.' + return + end + + index = contents.index(ANCHOR) + if index.nil? + Pod::UI.warn 'fmt: could not find `#if FMT_USE_CONSTEVAL` in base.h; ' \ + 'the consteval patch was NOT applied. If this fmt version ' \ + 'still uses a consteval format-string check, builds on ' \ + 'Xcode >= 26.2 will fail -- update ' \ + 'ios/scripts/patch_fmt_consteval.rb.' + return + end + + contents.insert(index, OVERRIDE) + + # CocoaPods checks pod sources out read-only (0444), so make the header + # writable for the write and restore the original mode afterwards. + mode = File.stat(header).mode & 0o7777 + begin + File.chmod(mode | 0o200, header) + File.write(header, contents) + ensure + File.chmod(mode, header) + end + + Pod::UI.puts 'fmt: patched base.h to set FMT_USE_CONSTEVAL=0 (Xcode >= 26.2 compatibility).' + end +end