From 0a7f8631b65e5f9a01f1d59d15468637ef247769 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 22 Dec 2025 18:59:36 +0500 Subject: [PATCH 1/6] mobile: add home shortcuts Add ability to create shortcuts on launcher homescreen for notes, notebooks and tags. --- .../notesnook/RCTNNativeModule.java | 180 ++++++++++++++++++ .../main/res/drawable-anydpi/ic_notebook.xml | 11 ++ .../main/res/drawable-hdpi/ic_notebook.png | Bin 0 -> 284 bytes .../main/res/drawable-mdpi/ic_notebook.png | Bin 0 -> 211 bytes .../main/res/drawable-xhdpi/ic_notebook.png | Bin 0 -> 440 bytes .../main/res/drawable-xxhdpi/ic_notebook.png | Bin 0 -> 807 bytes .../app/components/properties/items.tsx | 18 +- apps/mobile/app/hooks/use-actions.tsx | 25 ++- apps/mobile/app/hooks/use-app-events.tsx | 46 +++-- .../app/screens/editor/tiptap/use-editor.ts | 5 +- apps/mobile/app/utils/notesnook-module.ts | 19 +- apps/mobile/scripts/optimize-fonts.mjs | 3 +- packages/intl/locale/en.po | 4 + packages/intl/locale/pseudo-LOCALE.po | 4 + packages/intl/src/strings.ts | 3 +- 15 files changed, 291 insertions(+), 27 deletions(-) create mode 100644 apps/mobile/android/app/src/main/res/drawable-anydpi/ic_notebook.xml create mode 100644 apps/mobile/android/app/src/main/res/drawable-hdpi/ic_notebook.png create mode 100644 apps/mobile/android/app/src/main/res/drawable-mdpi/ic_notebook.png create mode 100644 apps/mobile/android/app/src/main/res/drawable-xhdpi/ic_notebook.png create mode 100644 apps/mobile/android/app/src/main/res/drawable-xxhdpi/ic_notebook.png 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 b79daf2ac..9cd572e67 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 @@ -6,7 +6,14 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +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; import android.provider.Settings; @@ -237,6 +244,179 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { } } + @ReactMethod + public void addShortcut(final String id, final String type, final String title, final String description, Promise promise) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher"); + return; + } + try { + ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class); + if (shortcutManager == null) { + promise.reject("ERROR", "ShortcutManager not available"); + return; + } + + String uri = "https://app.notesnook.com/open_" + type + "?id=" + id; + Intent intent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(uri)); + intent.setPackage(mContext.getPackageName()); + + Icon icon = createLetterIcon(type, title); + + ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, id) + .setShortLabel(title) + .setLongLabel(description != null && !description.isEmpty() ? description : title) + .setIcon(icon) + .setIntent(intent) + .build(); + + shortcutManager.requestPinShortcut(shortcut, null); + promise.resolve(true); + } catch (Exception e) { + promise.reject("ERROR", e.getMessage()); + } + } + + @ReactMethod + public void removeShortcut(final String id, Promise promise) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher"); + return; + } + + try { + ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class); + if (shortcutManager == null) { + promise.reject("ERROR", "ShortcutManager not available"); + return; + } + + shortcutManager.disableShortcuts(java.util.Collections.singletonList(id)); + promise.resolve(true); + } catch (Exception e) { + promise.reject("ERROR", e.getMessage()); + } + } + + @ReactMethod + public void updateShortcut(final String id, final String title, final String description, Promise promise) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher"); + return; + } + + try { + ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class); + if (shortcutManager == null) { + promise.reject("ERROR", "ShortcutManager not available"); + return; + } + + // Get existing shortcut to preserve icon and intent + List shortcuts = shortcutManager.getPinnedShortcuts(); + ShortcutInfo existingShortcut = null; + for (ShortcutInfo s : shortcuts) { + if (s.getId().equals(id)) { + existingShortcut = s; + break; + } + } + + if (existingShortcut == null) { + promise.reject("NOT_FOUND", "Shortcut not found"); + return; + } + + ShortcutInfo updatedShortcut = new ShortcutInfo.Builder(mContext, id) + .setShortLabel(title) + .setLongLabel(description != null && !description.isEmpty() ? description : title) + .setIntent(existingShortcut.getIntent()) + .build(); + + shortcutManager.updateShortcuts(java.util.Collections.singletonList(updatedShortcut)); + promise.resolve(true); + } catch (Exception e) { + promise.reject("ERROR", e.getMessage()); + } + } + + @ReactMethod + public void removeAllShortcuts(Promise promise) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher"); + return; + } + + try { + ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class); + if (shortcutManager == null) { + promise.reject("ERROR", "ShortcutManager not available"); + return; + } + + List pinnedShortcuts = shortcutManager.getPinnedShortcuts(); + if (!pinnedShortcuts.isEmpty()) { + List ids = new ArrayList<>(); + for (ShortcutInfo shortcut : pinnedShortcuts) { + ids.add(shortcut.getId()); + } + shortcutManager.disableShortcuts(ids); + } + promise.resolve(true); + } catch (Exception e) { + promise.reject("ERROR", e.getMessage()); + } + } + + private Icon createLetterIcon(String type, String title) { + + String letter = type.contains("tag") ? "#" : title != null && !title.isEmpty() + ? title.substring(0, 1).toUpperCase() + : "?"; + int color = getColorForLetter(letter); + + if (type.equals("notebook")) return Icon.createWithResource(mContext, R.drawable.ic_notebook); + // Use a larger canvas and fill it completely to avoid white borders from launcher masking. + int iconSize = 256; + Bitmap bitmap = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + + + Paint textPaint = new Paint(); + textPaint.setColor(color); + textPaint.setTextSize(130); + textPaint.setAntiAlias(true); + textPaint.setTextAlign(Paint.Align.CENTER); + textPaint.setTypeface(android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD)); + + float x = iconSize / 2f; + float y = (iconSize / 2f) - ((textPaint.descent() + textPaint.ascent()) / 2f); + + canvas.drawText(letter, x, y, textPaint); + + return Icon.createWithBitmap(bitmap); + } + + private int getColorForLetter(String letter) { + int[] colors = { + 0xFF1976D2, // Blue + 0xFFD32F2F, // Red + 0xFF388E3C, // Green + 0xFFF57C00, // Orange + 0xFF7B1FA2, // Purple + 0xFF0097A7, // Cyan + 0xFFC2185B, // Pink + 0xFF455A64, // Blue Grey + 0xFF6A1B9A, // Deep Purple + 0xFF00796B, // Teal + 0xFF512DA8, // Indigo + 0xFF1565C0 // Dark Blue + }; + + int hash = Math.abs(letter.hashCode()); + return colors[hash % colors.length]; + } } diff --git a/apps/mobile/android/app/src/main/res/drawable-anydpi/ic_notebook.xml b/apps/mobile/android/app/src/main/res/drawable-anydpi/ic_notebook.xml new file mode 100644 index 000000000..89dcc5c15 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/drawable-anydpi/ic_notebook.xml @@ -0,0 +1,11 @@ + + + diff --git a/apps/mobile/android/app/src/main/res/drawable-hdpi/ic_notebook.png b/apps/mobile/android/app/src/main/res/drawable-hdpi/ic_notebook.png new file mode 100644 index 0000000000000000000000000000000000000000..16da59a349c4bdd618d851f5796bbb81e776c85d GIT binary patch literal 284 zcmV+%0ptFOP)*r<40U%}2P?mu8dgI^&<{ z`+n`cKgAgDHR`FgVxK`^c*BCV3fepNQwbs5Sg2M(`^tVQQh?Va*zJN1OS9A;$u*Ud zC}V+Gl(9f8%2*&4Wxg&DK63N?9>N`+bFR+zv=8Z2<&8wIwrc$E{dTTu|Wv0000$UubQwj&Gs z33m^p36nYe6!WsXSYEZZZI*Ak)XeU?qx$DdlfSZxEy0>kH>OXU_S;IL*2wa?_4iVf znct_*-1m!yta@1u5}d4uFFan=884G z+rm+@+;xGqF4Nsi!54nr4&^|p>__(=?(36yJagjQrBC!I zYS2DofAo$Ic3tltg*|fHyY?=(BiHRk-KQFi_-7p`d3Du2OnBen#PsxbBc8)&ZO-$* z-c^@lc(HW;+zP&#sgd_KJh?R)$m|rbT~xFBWS))x>?77wyPj%w-tPd-~zNUaccBraJAv%~lDJu0TA0#`_;{)a7q)4{V)KJ#*R1DZ=l~ zr{Ay1itp4q^ZuE3;Thd`#|roSu5vv-_g45KuE!1j9}AB>zi-j@zK8$nKB3(c65em- z=KN+yEyf?=eNR)&=Pf@wZF;pu<<0iz6MtNuUgH^8+4e!wd!6aW)AQF}Ut?|ke}+Hv Xn=b*y@828;#t4I_tDnm{r-UW|3vShE literal 0 HcmV?d00001 diff --git a/apps/mobile/android/app/src/main/res/drawable-xxhdpi/ic_notebook.png b/apps/mobile/android/app/src/main/res/drawable-xxhdpi/ic_notebook.png new file mode 100644 index 0000000000000000000000000000000000000000..5b16af02d36f8cbda9a10da4766414bfcf6ab664 GIT binary patch literal 807 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2V0!22;uum9_jbdoV?>v(B#|4u}?S$KH! z!2|<|Khu3Kf2oR`=D+*yyYs4Xcf-u~J$lD--SuB)!sbDbpzFxVcnjx73-hpUj3T`)Xd~ zwD`*=dspxOlx?eDymt>Tmuynm)!EN~uB&+1K}{CNNA{7c^b+xjO}M}KbLbLaVuN)ZvOBTsifbKaR= z^H;z${+)4re!=|Y=Yatd!rcY&IqI#38#%roEejgT+-z&R{<*1zopr0EN?w?*IS* literal 0 HcmV?d00001 diff --git a/apps/mobile/app/components/properties/items.tsx b/apps/mobile/app/components/properties/items.tsx index 3a9def97e..34082fd27 100644 --- a/apps/mobile/app/components/properties/items.tsx +++ b/apps/mobile/app/components/properties/items.tsx @@ -57,6 +57,7 @@ const BOTTOM_BAR_ITEMS: ActionId[] = [ "export", "copy-link", "duplicate", + "launcher-shortcut", "trash" ]; @@ -76,6 +77,7 @@ const COLUMN_BAR_ITEMS: ActionId[] = [ "reorder", "rename-color", "rename-tag", + "launcher-shortcut", "restore", "trash", "delete" @@ -170,15 +172,15 @@ export const Items = ({ DDS.isTab ? AppFontSize.xxl : shouldShrink - ? AppFontSize.xxl - : AppFontSize.lg + ? AppFontSize.xxl + : AppFontSize.lg } color={ item.checked ? item.activeColor || colors.primary.accent : item.id.match(/(delete|trash)/g) - ? colors.error.icon - : colors.secondary.icon + ? colors.error.icon + : colors.secondary.icon } /> @@ -209,8 +211,8 @@ export const Items = ({ text: item.checked ? item.activeColor || colors.primary.accent : item.id === "delete" || item.id === "trash" - ? colors.error.paragraph - : colors.primary.paragraph + ? colors.error.paragraph + : colors.primary.paragraph }} testID={"icon-" + item.id} onPress={item.onPress} @@ -274,8 +276,8 @@ export const Items = ({ item.checked ? item.activeColor || colors.primary.accent : item.id === "delete" || item.id === "trash" - ? colors.error.icon - : colors.secondary.icon + ? colors.error.icon + : colors.secondary.icon } /> diff --git a/apps/mobile/app/hooks/use-actions.tsx b/apps/mobile/app/hooks/use-actions.tsx index c48c5c497..ebc83e520 100644 --- a/apps/mobile/app/hooks/use-actions.tsx +++ b/apps/mobile/app/hooks/use-actions.tsx @@ -23,6 +23,7 @@ import { Item, ItemReference, Note, + Notebook, VAULT_ERRORS } from "@notesnook/core"; import { strings } from "@notesnook/intl"; @@ -70,6 +71,7 @@ import { deleteItems } from "../utils/functions"; import { convertNoteToText } from "../utils/note-to-text"; import { sleep } from "../utils/time"; import { resetStoredState } from "./use-stored-state"; +import { NotesnookModule } from "../utils/notesnook-module"; export type ActionId = | "select" @@ -114,7 +116,8 @@ export type ActionId = | "remove-from-notebook" | "trash" | "default-homepage" - | "default-tag"; + | "default-tag" + | "launcher-shortcut"; export type Action = { id: ActionId; @@ -1207,5 +1210,25 @@ export const useActions = ({ }); } + if ( + Platform.OS === "android" && + (item.type === "tag" || item.type === "note" || item.type === "notebook") + ) { + actions.push({ + id: "launcher-shortcut", + title: strings.addToHome(), + icon: "cellphone-arrow-down", + onPress: async () => { + const added = await NotesnookModule.addShortcut( + item.id, + item.type, + item.title, + (item as Note).headline || (item as Notebook).description || "" + ); + console.log(added); + } + }); + } + return actions; }; diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index 83f20095b..62765f78b 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -171,7 +171,7 @@ const onAppOpenedFromURL = async (event: { url: string }) => { eSendEvent(eOnLoadNote, { newNote: true }); fluidTabsRef.current?.goToPage("editor", false); return; - } else if (url.startsWith("https://app.notesnook.com/open_note")) { + } else if (url.startsWith("https://app.notesnook.com/open_note?")) { const id = new URL(url).searchParams.get("id"); if (id) { const note = await db.notes.note(id); @@ -182,6 +182,26 @@ const onAppOpenedFromURL = async (event: { url: string }) => { fluidTabsRef.current?.goToPage("editor", false); } } + } else if (url.startsWith("https://app.notesnook.com/open_notebook?")) { + const id = new URL(url).searchParams.get("id"); + if (id) { + const notebook = await db.notebooks.notebook(id); + if (notebook) { + Navigation.navigate("Notebook", { + item: notebook + }); + } + } + } else if (url.startsWith("https://app.notesnook.com/open_tag?")) { + const id = new URL(url).searchParams.get("id"); + if (id) { + const tag = await db.tags.tag(id); + if (tag) { + Navigation.navigate("TaggedNotes", { + item: tag + }); + } + } } else if (url.startsWith("https://app.notesnook.com/open_reminder")) { const id = new URL(url).searchParams.get("id"); if (id) { @@ -774,23 +794,21 @@ export const useAppEvents = () => { } }; - if (!refValues.current.initialUrl) { - Linking.getInitialURL().then((url) => { - if (url) { - refValues.current.initialUrl = url; - } - }); - } let sub: NativeEventSubscription; if (!isAppLoading && !appLocked) { + if (!refValues.current.initialUrl) { + Linking.getInitialURL().then((url) => { + if (url) { + refValues.current.initialUrl = url; + onAppOpenedFromURL({ + url: refValues.current.initialUrl! + }); + } + }); + } + setTimeout(() => { sub = AppState.addEventListener("change", onAppStateChanged); - if (refValues.current.initialUrl) { - onAppOpenedFromURL({ - url: refValues.current.initialUrl! - }); - refValues.current.initialUrl = undefined; - } }, 1000); refValues.current.removeInternetStateListener = NetInfo.addEventListener( diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor.ts b/apps/mobile/app/screens/editor/tiptap/use-editor.ts index a9ce25b60..ea1afaee8 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-editor.ts @@ -1088,7 +1088,10 @@ export const useEditor = ( if (!state.current?.initialLoadCalled) { const url = await Linking.getInitialURL(); - let noteId = url && new URL(url).searchParams.get("id"); + let noteId = + url && + url.startsWith("https://app.notesnook.com/open_note?") && + new URL(url).searchParams.get("id"); if (noteId) { const note = await db.notes?.note(noteId); fluidTabsRef.current?.goToPage("editor"); diff --git a/apps/mobile/app/utils/notesnook-module.ts b/apps/mobile/app/utils/notesnook-module.ts index cf99eebac..aa337f35e 100644 --- a/apps/mobile/app/utils/notesnook-module.ts +++ b/apps/mobile/app/utils/notesnook-module.ts @@ -41,6 +41,19 @@ interface NotesnookModuleInterface { updateWidgetNote: (noteId: string, data: string) => void; updateReminderWidget: () => void; isGestureNavigationEnabled: () => boolean; + addShortcut: ( + id: string, + type: "note" | "notebook" | "tag", + title: string, + description?: string + ) => Promise; + removeShortcut: (id: string) => Promise; + updateShortcut: ( + id: string, + title: string, + description?: string + ) => Promise; + removeAllShortcuts: () => Promise; } export const NotesnookModule: NotesnookModuleInterface = Platform.select({ @@ -61,7 +74,11 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({ hasWidgetNote: () => {}, updateWidgetNote: () => {}, updateReminderWidget: () => {}, - isGestureNavigationEnabled: () => true + isGestureNavigationEnabled: () => true, + addShortcut: () => Promise.resolve(false), + removeShortcut: () => Promise.resolve(false), + updateShortcut: () => Promise.resolve(false), + removeAllShortcuts: () => Promise.resolve(false) }, android: NativeModules.NNativeModule }); diff --git a/apps/mobile/scripts/optimize-fonts.mjs b/apps/mobile/scripts/optimize-fonts.mjs index a16fdcaf7..dc748cb14 100644 --- a/apps/mobile/scripts/optimize-fonts.mjs +++ b/apps/mobile/scripts/optimize-fonts.mjs @@ -115,7 +115,8 @@ const EXTRA_ICON_NAMES = [ "update", "notebook-minus", "calendar-blank", - "email-newsletter" + "email-newsletter", + "cellphone-arrow-down" ]; const __filename = fileURLToPath(import.meta.url); diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index 9cab86210..b40a97b48 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -735,6 +735,10 @@ msgstr "Add tags to multiple notes at once" msgid "Add to dictionary" msgstr "Add to dictionary" +#: src/strings.ts:2613 +msgid "Add to home" +msgstr "Add to home" + #: src/strings.ts:2476 msgid "Add to notebook" msgstr "Add to notebook" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 4b597f800..669131bca 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -735,6 +735,10 @@ msgstr "" msgid "Add to dictionary" msgstr "" +#: src/strings.ts:2613 +msgid "Add to home" +msgstr "" + #: src/strings.ts:2476 msgid "Add to notebook" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index f71e13b4d..525f176e0 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2609,5 +2609,6 @@ Use this if changes from other devices are not appearing on this device. This wi views: () => t`Views`, clickToUpdate: () => t`Click to update`, noPassword: () => t`No password`, - publishToTheWeb: () => t`Publish to the web` + publishToTheWeb: () => t`Publish to the web`, + addToHome: () => t`Add to home` }; From b59a3fb5dee5e202cbd206f8a84e20fec80c74cc Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Tue, 23 Dec 2025 10:54:46 +0500 Subject: [PATCH 2/6] mobile: improved navigation for deep links --- apps/mobile/app/app.tsx | 11 +- apps/mobile/app/components/header/index.tsx | 13 +- apps/mobile/app/hooks/use-actions.tsx | 20 --- apps/mobile/app/hooks/use-app-events.tsx | 49 ++++-- .../app/navigation/navigation-stack.tsx | 165 +++++++++--------- apps/mobile/app/screens/notebook/index.tsx | 76 ++++---- apps/mobile/app/screens/notes/colored.tsx | 14 +- apps/mobile/app/screens/notes/index.tsx | 62 +++---- apps/mobile/app/screens/notes/monographs.tsx | 3 +- apps/mobile/app/screens/notes/tagged.tsx | 12 +- .../mobile/app/stores/use-navigation-store.ts | 10 +- apps/mobile/app/stores/use-setting-store.ts | 4 +- 12 files changed, 221 insertions(+), 218 deletions(-) diff --git a/apps/mobile/app/app.tsx b/apps/mobile/app/app.tsx index 9f439bcec..859889967 100644 --- a/apps/mobile/app/app.tsx +++ b/apps/mobile/app/app.tsx @@ -24,7 +24,7 @@ import { useThemeEngineStore } from "@notesnook/theme"; import React, { useEffect } from "react"; -import { Appearance, I18nManager, StatusBar } from "react-native"; +import { Appearance, I18nManager, Linking, StatusBar } from "react-native"; import "react-native-gesture-handler"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { SafeAreaProvider } from "react-native-safe-area-context"; @@ -43,7 +43,7 @@ import { changeSystemBarColors, useThemeStore } from "./stores/use-theme-store"; 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"; I18nManager.allowRTL(false); I18nManager.forceRTL(false); I18nManager.swapLeftAndRightInRTL(false); @@ -54,13 +54,18 @@ if (appLockEnabled || appLockMode !== "none") { RNBootSplash.hide({ fade: true }); +Linking.getInitialURL().then((url) => { + useSettingStore.setState({ + initialUrl: url + }); +}); const App = (props: { configureMode: "note-preview" }) => { useAppEvents(); //@ts-ignore globalThis["IS_MAIN_APP_RUNNING"] = true; useEffect(() => { - changeSystemBarColors(); SettingsService.onFirstLaunch(); + changeSystemBarColors(); setTimeout(async () => { await Notifications.get(); if (SettingsService.get().notifNotes) { diff --git a/apps/mobile/app/components/header/index.tsx b/apps/mobile/app/components/header/index.tsx index 5b64d97f3..41a9787b0 100644 --- a/apps/mobile/app/components/header/index.tsx +++ b/apps/mobile/app/components/header/index.tsx @@ -50,7 +50,7 @@ export const Header = ({ onLeftMenuButtonPress?: () => void; renderedInRoute?: RouteName; id?: string; - title: string; + title?: string; canGoBack?: boolean; onPressDefaultRightButton?: () => void; hasSearch?: boolean; @@ -114,7 +114,16 @@ export const Header = ({ onLeftButtonPress={onLeftMenuButtonPress} /> - {hasSearch ? ( + {!title ? ( + + ) : hasSearch ? ( {selectionMode ? `${selectedItemsList.length} selected` diff --git a/apps/mobile/app/hooks/use-actions.tsx b/apps/mobile/app/hooks/use-actions.tsx index ebc83e520..f52ad205e 100644 --- a/apps/mobile/app/hooks/use-actions.tsx +++ b/apps/mobile/app/hooks/use-actions.tsx @@ -709,26 +709,6 @@ export const useActions = ({ type: item.type } ); - - resetStoredState( - "app-home-navigtion-key", - isHomepage - ? undefined - : { - name: - item.type === "notebook" - ? "Notebook" - : item.type === "tag" - ? "TaggedNotes" - : item.type === "color" - ? "ColorNotes" - : undefined, - params: { - item: item, - id: item.id - } - } - ); } }); } diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index 62765f78b..7c572240d 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -85,7 +85,7 @@ import Notifications from "../services/notifications"; import PremiumService from "../services/premium"; import SettingsService from "../services/settings"; import Sync from "../services/sync"; -import { clearAllStores, initAfterSync, initialize } from "../stores"; +import { clearAllStores, initAfterSync } from "../stores"; import { refreshAllStores } from "../stores/create-db-collection-store"; import { useAttachmentStore } from "../stores/use-attachment-store"; import { useMessageStore } from "../stores/use-message-store"; @@ -159,7 +159,10 @@ const onUserSessionExpired = async () => { eSendEvent(eLoginSessionExpired); }; -const onAppOpenedFromURL = async (event: { url: string }) => { +const onAppOpenedFromURL = async (event: { + url: string; + isInitialUrl?: boolean; +}) => { const url = event.url; try { @@ -182,23 +185,34 @@ const onAppOpenedFromURL = async (event: { url: string }) => { fluidTabsRef.current?.goToPage("editor", false); } } - } else if (url.startsWith("https://app.notesnook.com/open_notebook?")) { + } else if ( + url.startsWith("https://app.notesnook.com/open_notebook?") && + !event.isInitialUrl + ) { const id = new URL(url).searchParams.get("id"); if (id) { const notebook = await db.notebooks.notebook(id); if (notebook) { Navigation.navigate("Notebook", { + id: notebook.id, + canGoBack: true, item: notebook }); } } - } else if (url.startsWith("https://app.notesnook.com/open_tag?")) { + } else if ( + url.startsWith("https://app.notesnook.com/open_tag?") && + !event.isInitialUrl + ) { const id = new URL(url).searchParams.get("id"); if (id) { const tag = await db.tags.tag(id); if (tag) { Navigation.navigate("TaggedNotes", { - item: tag + type: "tag", + id: tag.id, + item: tag, + canGoBack: true }); } } @@ -484,7 +498,10 @@ const initializeDatabase = async (password?: string) => { }; export const useAppEvents = () => { - const isAppLoading = useSettingStore((state) => state.isAppLoading); + const [isAppLoading, initialUrl] = useSettingStore((state) => [ + state.isAppLoading, + state.initialUrl + ]); const [setLastSynced, setUser, appLocked, syncing] = useUserStore((state) => [ state.setLastSynced, state.setUser, @@ -529,6 +546,15 @@ export const useAppEvents = () => { }; }, [isAppLoading, onSyncComplete]); + useEffect(() => { + if (initialUrl) { + onAppOpenedFromURL({ + url: initialUrl!, + isInitialUrl: true + }); + } + }, [initialUrl]); + const subscribeToPurchaseListeners = useCallback(async () => { if (Platform.OS === "android") { try { @@ -796,17 +822,6 @@ export const useAppEvents = () => { let sub: NativeEventSubscription; if (!isAppLoading && !appLocked) { - if (!refValues.current.initialUrl) { - Linking.getInitialURL().then((url) => { - if (url) { - refValues.current.initialUrl = url; - onAppOpenedFromURL({ - url: refValues.current.initialUrl! - }); - } - }); - } - setTimeout(() => { sub = AppState.addEventListener("change", onAppStateChanged); }, 1000); diff --git a/apps/mobile/app/navigation/navigation-stack.tsx b/apps/mobile/app/navigation/navigation-stack.tsx index 8b5d12a7c..9884182ee 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -31,8 +31,6 @@ import { useSettingStore } from "../stores/use-setting-store"; import { rootNavigatorRef } from "../utils/global-refs"; import Navigation from "../services/navigation"; import { isFeatureAvailable } from "@notesnook/common"; -import { useStoredValue } from "../hooks/use-stored-state"; -import { db } from "../common/database"; const RootStack = createNativeStackNavigator(); const AppStack = createNativeStackNavigator(); @@ -58,105 +56,102 @@ const AppNavigation = React.memo( () => { const { colors } = useThemeColors(); const homepageV2 = useSettingStore((state) => state.settings.homepageV2); - const home = useStoredValue( - "app-home-navigtion-key", - !homepageV2 || homepageV2.id === DEFAULT_HOME.name - ? DEFAULT_HOME - : undefined - ); const loading = useSettingStore((state) => state.isAppLoading); + const [home, setHome] = React.useState< + { name: string; params: any } | undefined + >(undefined); - if (!home.value && !homepageV2) { - home.value = DEFAULT_HOME; - } + React.useEffect(() => { + if (useSettingStore.getState().initialUrl) { + const url = useSettingStore.getState().initialUrl; + if (url?.startsWith("https://app.notesnook.com/open_notebook?")) { + const id = new URL(url).searchParams.get("id"); + if (id) { + setHome({ + name: "Notebook", + params: { + id: id + } + }); + return; + } + } else if (url?.startsWith("https://app.notesnook.com/open_tag?")) { + const id = new URL(url).searchParams.get("id"); + if (id) { + setHome({ + name: "TaggedNotes", + params: { + type: "tag", + id: id + } + }); + return; + } + } + } + + if (homepageV2) { + switch (homepageV2.type) { + case "notebook": { + setHome({ + name: "Notebook", + params: { + id: homepageV2.id + } + }); + return; + } + case "color": { + setHome({ + name: "ColoredNotes", + params: { + type: "color", + id: homepageV2.id + } + }); + return; + } + case "tag": { + setHome({ + name: "TaggedNotes", + params: { + type: "tag", + id: homepageV2.id + } + }); + return; + } + case "default": + setHome(DEFAULT_HOME); + return; + } + } else { + setHome(DEFAULT_HOME); + } + }, []); React.useEffect(() => { if (!homepageV2 || loading) return; (async () => { isFeatureAvailable("customHomepage").then((value) => { if (!value.isAllowed) { - home.value = DEFAULT_HOME; SettingsService.setProperty("homepageV2", undefined); } }); - - if (!home.value) { - switch (homepageV2.type) { - case "notebook": - { - const notebook = await db.notebooks.notebook(homepageV2.id); - if (notebook) { - home.value = { - name: "Notebook", - params: { - item: notebook, - id: notebook.id, - title: notebook.title - } - }; - return; - } - } - break; - case "color": { - const color = await db.colors.color(homepageV2.id); - if (color) { - home.value = { - name: "ColoredNotes", - params: { - item: color, - id: color.id, - title: color.title - } - }; - return; - } - - break; - } - case "tag": { - const tag = await db.tags.tag(homepageV2.id); - if (tag) { - home.value = { - name: "TaggedNotes", - params: { - item: tag, - id: tag.id, - title: tag.title - } - }; - return; - } - break; - } - case "default": - { - home.value = DEFAULT_HOME; - } - return; - } - - home.value = undefined; - setTimeout(() => { - home.value = DEFAULT_HOME; - }); - } })(); - }, [homepageV2, loading, home.value]); + }, [homepageV2, loading]); React.useEffect(() => { if (!home) return; + useNavigationStore.getState().update(home?.name as keyof RouteParams); useNavigationStore .getState() - .update(home.value?.name as keyof RouteParams); - useNavigationStore - .getState() - .setFocusedRouteId(home.value?.params?.id || home.value?.name); + .setFocusedRouteId(home?.params?.id || home?.name); }, [home]); - return !home.value ? null : ( + return !home ? null : ( @@ -209,7 +204,7 @@ const AppNavigation = React.memo( return ColoredNotes; }} initialParams={ - home.value?.name === "ColoredNotes" ? home.value?.params : undefined + home?.name === "ColoredNotes" ? home?.params : undefined } /> @@ -244,9 +239,7 @@ const AppNavigation = React.memo( Notebook = Notebook || require("../screens/notebook").default; return Notebook; }} - initialParams={ - home.value?.name === "Notebook" ? home.value?.params : undefined - } + initialParams={home?.name === "Notebook" ? home?.params : undefined} /> ) => { const [notes, setNotes] = useState>(); const params = useRef(route?.params); const isAppLoading = useSettingStore((state) => state.isAppLoading); + const [notebook, setNotebook] = useState( + params.current.item + ); const [loading, setLoading] = useState(true); const updateOnFocus = useRef(false); const [breadcrumbs, setBreadcrumbs] = useState< @@ -75,10 +77,10 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { }); const syncWithNavigation = React.useCallback(() => { - useNavigationStore.getState().setFocusedRouteId(params?.current?.item?.id); + useNavigationStore.getState().setFocusedRouteId(params?.current?.id); setOnFirstSave({ type: "notebook", - id: params.current.item.id + id: params.current.id }); }, []); @@ -86,23 +88,20 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { async (data?: NotebookScreenParams) => { if (useSettingStore.getState().isAppLoading) return; if ( - useNavigationStore.getState().focusedRouteId !== - params.current.item.id && + useNavigationStore.getState().focusedRouteId !== params.current.id && !data ) { updateOnFocus.current = true; return; } - if (data?.item?.id && params.current.item?.id !== data?.item?.id) { - const nextRootNotebookId = await findRootNotebookId(data?.item?.id); - const currentNotebookRoot = await findRootNotebookId( - params.current.item.id - ); + if (data?.id && params.current?.id !== data?.id) { + const nextRootNotebookId = await findRootNotebookId(data?.id); + const currentNotebookRoot = await findRootNotebookId(params.current.id); if ( nextRootNotebookId !== currentNotebookRoot || - nextRootNotebookId === params.current?.item?.id + nextRootNotebookId === params.current?.id ) { // Never update notebook in route if root is different or if the root is current notebook. return; @@ -112,14 +111,13 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { if (data) params.current = data; try { - const notebook = await db.notebooks?.notebook( - params?.current?.item?.id - ); - + const notebook = await db.notebooks?.notebook(params?.current?.id); + setNotebook(notebook); + params.current.item = notebook; if (notebook) { const breadcrumbs = await db.notebooks.breadcrumbs(notebook.id); setBreadcrumbs(breadcrumbs.slice(0, breadcrumbs.length - 1)); - params.current.item = notebook; + params.current.id = notebook.id; const notes = await db.relations .from(notebook, "note") .selector.grouped(db.settings.getGroupOptions("notes")); @@ -160,29 +158,27 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { <>
{ - Properties.present(params.current.item); + Properties.present(notebook); } }} hasSearch={true} onSearch={() => { - const selector = db.relations.from( - params.current.item, - "note" - ).selector; + if (!notebook) return; + const selector = db.relations.from(notebook, "note").selector; Navigation.push("Search", { - placeholder: strings.searchInRoute(params.current.item?.title), + placeholder: strings.searchInRoute(notebook?.title), type: "note", - title: params.current.item?.title, + title: notebook?.title, route: route.name, items: selector }); }} - id={params.current.item?.id} + id={notebook?.id} /> @@ -192,19 +188,19 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { onRefresh={() => { onRequestUpdate(); }} - id={params.current.item?.id} + id={params.current?.id} renderedInRoute="Notebook" - headerTitle={params.current.item.title} + headerTitle={notebook?.title} loading={loading} CustomLisHeader={ } placeholder={{ - title: params.current.item?.title, + title: notebook?.title!, paragraph: strings.notesEmpty(), button: strings.addFirstNote(), action: openEditor, @@ -225,7 +221,8 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { testID="notebookTreeSheet" size="small" onPress={() => { - Notebooks.present(params.current.item); + if (!notebook) return; + Notebooks.present(notebook); }} style={{ position: "relative", @@ -246,7 +243,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { /> { const { currentRoute, focusedRouteId } = useNavigationStore.getState(); if (currentRoute === "Notebooks") { Navigation.push("Notebook", { + id: item.id, item: item, canGoBack }); @@ -276,22 +274,24 @@ NotebookScreen.navigate = async (item: Notebook, canGoBack?: boolean) => { // Update the route in place instead eSendEvent(eUpdateNotebookRoute, { - item: item, - title: item.title, - canGoBack: canGoBack + id: item.id, + canGoBack: canGoBack, + item: item }); } else { // Push a new route Navigation.push("Notebook", { - item: item, - canGoBack + id: item.id, + canGoBack, + item: item }); } } else { // Push a new route anyways Navigation.push("Notebook", { - item: item, - canGoBack + id: item.id, + canGoBack, + item: item }); } }; diff --git a/apps/mobile/app/screens/notes/colored.tsx b/apps/mobile/app/screens/notes/colored.tsx index 771289e3d..3f727b7d7 100644 --- a/apps/mobile/app/screens/notes/colored.tsx +++ b/apps/mobile/app/screens/notes/colored.tsx @@ -25,7 +25,7 @@ import Navigation, { NavigationProps } from "../../services/navigation"; import useNavigationStore, { NotesScreenParams } from "../../stores/use-navigation-store"; -import { PLACEHOLDER_DATA, openEditor, toCamelCase } from "./common"; +import { PLACEHOLDER_DATA, openEditor } from "./common"; export const ColoredNotes = ({ navigation, route @@ -45,11 +45,13 @@ export const ColoredNotes = ({ ColoredNotes.get = async (params: NotesScreenParams, grouped = true) => { if (!grouped) { - return await db.relations.from(params.item, "note").resolve(); + return await db.relations + .from({ id: params.id, type: "color" }, "note") + .resolve(); } return await db.relations - .from(params.item, "note") + .from({ id: params.id, type: "color" }, "note") .selector.grouped(db.settings.getGroupOptions("notes")); }; @@ -63,8 +65,10 @@ ColoredNotes.navigate = (item: Color, canGoBack: boolean) => { } Navigation.push<"ColoredNotes">("ColoredNotes", { - item: item, - canGoBack + type: "color", + id: item.id, + canGoBack, + item: item }); }; diff --git a/apps/mobile/app/screens/notes/index.tsx b/apps/mobile/app/screens/notes/index.tsx index bc243054a..99d75a4ee 100644 --- a/apps/mobile/app/screens/notes/index.tsx +++ b/apps/mobile/app/screens/notes/index.tsx @@ -18,7 +18,7 @@ along with this program. If not, see . */ import { resolveItems } from "@notesnook/common"; -import { VirtualizedGrouping } from "@notesnook/core"; +import { Tag, VirtualizedGrouping } from "@notesnook/core"; import { Color, Note } from "@notesnook/core"; import React, { useEffect, useRef, useState } from "react"; import { db } from "../../common/database"; @@ -76,14 +76,12 @@ const NotesPage = ({ const [notes, setNotes] = useState>(); const [loadingNotes, setLoadingNotes] = useState(true); const isMonograph = route.name === "Monographs"; - const title = - params.current?.item.type === "tag" - ? "#" + params.current?.item.title - : params.current?.item.title; + const [item, setItem] = useState( + params.current.item + ); + const title = item?.type === "tag" ? "#" + item?.title : item?.title; const accentColor = - route.name === "ColoredNotes" - ? (params.current?.item as Color)?.colorCode - : undefined; + route.name === "ColoredNotes" ? (item as Color)?.colorCode : undefined; const updateOnFocus = useRef(false); const isAppLoading = useSettingStore((state) => state.isAppLoading); const isFocused = useNavigationFocus(navigation, { @@ -107,31 +105,30 @@ const NotesPage = ({ }); const syncWithNavigation = React.useCallback(() => { - const { item } = params.current; - useNavigationStore - .getState() - .setFocusedRouteId(params?.current?.item?.id || route.name); + const { id } = params.current; + useNavigationStore.getState().setFocusedRouteId(id || route.name); !isMonograph && setOnFirstSave({ type: getItemType(route.name), - id: item.id + id: id }); }, [isMonograph, route.name]); const onRequestUpdate = React.useCallback( async (data?: NotesScreenParams) => { if (useSettingStore.getState().isAppLoading) return; + if ( - params.current.item.id && - useNavigationStore.getState().focusedRouteId !== - params.current.item.id && + params.current.id && + useNavigationStore.getState().focusedRouteId !== params.current.id && !data ) { updateOnFocus.current = false; return; } - const isNew = data && data?.item?.id !== params.current?.item?.id; + + const isNew = data && data?.id !== params.current?.id; if (data) params.current = data; try { @@ -142,9 +139,9 @@ const NotesPage = ({ )) as VirtualizedGrouping; if (route.name === "TaggedNotes" || route.name === "ColoredNotes") { - const item = await (db as any)[params.current.item.type + "s"][ - params.current.item.type - ](params.current.item.id); + const item = await (db as any)[params.current.type + "s"][ + params.current.type + ](params.current.id); if (!item) { if (rootNavigatorRef.canGoBack()) { @@ -154,7 +151,7 @@ const NotesPage = ({ } return; } - + setItem(item); params.current.item = item; } @@ -173,15 +170,7 @@ const NotesPage = ({ useEffect(() => { if (isAppLoading) return; if (loadingNotes) { - get(params.current, true) - .then(async (items) => { - setNotes(items as VirtualizedGrouping); - await (items as VirtualizedGrouping).item(0, resolveItems); - setLoadingNotes(false); - }) - .catch((e) => { - setLoadingNotes(false); - }); + onRequestUpdate(params.current); } }, [loadingNotes, get, isAppLoading]); @@ -202,19 +191,18 @@ const NotesPage = ({ } canGoBack={params?.current?.canGoBack} hasSearch={true} - id={ - route.name === "Monographs" ? "Monographs" : params?.current.item?.id - } + id={route.name === "Monographs" ? "Monographs" : params?.current?.id} onSearch={() => { + if (!item) return; const selector = route.name === "Monographs" ? db.monographs.all - : db.relations.from(params.current.item, "note").selector; + : db.relations.from(item, "note").selector; Navigation.push("Search", { placeholder: strings.searchInRoute(title || route.name), type: "note", - title: title, + title: title!, route: route.name, items: selector }); @@ -229,7 +217,7 @@ const NotesPage = ({ onRefresh={onRequestUpdate} loading={false} renderedInRoute={route.name} - id={params.current.item?.id} + id={params.current?.id} headerTitle={title || "Monographs"} customAccentColor={accentColor} placeholder={placeholder} @@ -246,7 +234,7 @@ const NotesPage = ({ ) : null} { Monographs.navigate = (canGoBack?: boolean) => { Navigation.navigate<"Monographs">("Monographs", { - item: { type: "monograph" } as any, + type: "monograph", + id: "monograph", canGoBack: canGoBack as boolean }); }; diff --git a/apps/mobile/app/screens/notes/tagged.tsx b/apps/mobile/app/screens/notes/tagged.tsx index b0d89ac44..5ad689407 100644 --- a/apps/mobile/app/screens/notes/tagged.tsx +++ b/apps/mobile/app/screens/notes/tagged.tsx @@ -46,11 +46,13 @@ export const TaggedNotes = ({ TaggedNotes.get = async (params: NotesScreenParams, grouped = true) => { if (!grouped) { - return await db.relations.from(params.item, "note").resolve(); + return await db.relations + .from({ id: params.id, type: "tag" }, "note") + .resolve(); } return await db.relations - .from(params.item, "note") + .from({ id: params.id, type: "tag" }, "note") .selector.grouped(db.settings.getGroupOptions("notes")); }; @@ -64,8 +66,10 @@ TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => { } Navigation.push<"TaggedNotes">("TaggedNotes", { - item: item, - canGoBack + id: item.id, + type: "tag", + canGoBack, + item: item }); }; diff --git a/apps/mobile/app/stores/use-navigation-store.ts b/apps/mobile/app/stores/use-navigation-store.ts index a385725b3..08984bce8 100644 --- a/apps/mobile/app/stores/use-navigation-store.ts +++ b/apps/mobile/app/stores/use-navigation-store.ts @@ -25,8 +25,7 @@ import { Note, Notebook, Reminder, - Tag, - TrashItem + Tag } from "@notesnook/core"; import { ParamListBase } from "@react-navigation/core"; import { create } from "zustand"; @@ -36,12 +35,15 @@ export type GenericRouteParam = { }; export type NotebookScreenParams = { - item: Notebook; + id: string; + item?: Notebook; canGoBack?: boolean; }; export type NotesScreenParams = { - item: Note | Notebook | Tag | Color | TrashItem | Reminder; + type: "tag" | "color" | "monograph"; + id: string; + item?: Tag | Color; canGoBack?: boolean; }; diff --git a/apps/mobile/app/stores/use-setting-store.ts b/apps/mobile/app/stores/use-setting-store.ts index bc17219a0..232ab903d 100644 --- a/apps/mobile/app/stores/use-setting-store.ts +++ b/apps/mobile/app/stores/use-setting-store.ts @@ -136,6 +136,7 @@ export interface SettingStore { dateFormat: string; dbPassword?: string; isOldAppLock: () => boolean; + initialUrl: string | null; } const { width, height } = Dimensions.get("window"); @@ -236,5 +237,6 @@ export const useSettingStore = create((set, get) => ({ }, insets: initialWindowMetrics?.insets ? initialWindowMetrics.insets - : { top: 0, right: 0, left: 0, bottom: 0 } + : { top: 0, right: 0, left: 0, bottom: 0 }, + initialUrl: null })); From 465e1af838f62f8e25d8acf4ffb462346ddcd1e4 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 24 Dec 2025 13:11:08 +0500 Subject: [PATCH 3/6] mobile: keep shortcuts synced with the app --- .../notesnook/RCTNNativeModule.java | 39 ++++++++++++- apps/mobile/app/hooks/use-actions.tsx | 16 +++--- apps/mobile/app/stores/index.ts | 57 ++++++++++++++++++- apps/mobile/app/utils/notesnook-module.ts | 11 +++- 4 files changed, 111 insertions(+), 12 deletions(-) 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 9cd572e67..a32e85852 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 @@ -36,6 +36,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; public class RCTNNativeModule extends ReactContextBaseJavaModule { @@ -263,11 +264,11 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { intent.setPackage(mContext.getPackageName()); Icon icon = createLetterIcon(type, title); - ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, id) .setShortLabel(title) .setLongLabel(description != null && !description.isEmpty() ? description : title) .setIcon(icon) + .setCategories(Set.of(type)) .setIntent(intent) .build(); @@ -324,7 +325,6 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { } if (existingShortcut == null) { - promise.reject("NOT_FOUND", "Shortcut not found"); return; } @@ -369,6 +369,41 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { } } + @ReactMethod + public void getAllShortcuts(Promise promise) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class); + WritableArray shortcuts = Arguments.createArray(); + List infos = shortcutManager.getPinnedShortcuts(); + + for (ShortcutInfo info: infos) { + WritableMap data = Arguments.createMap(); + data.putString("id", info.getId()); + if (info.getShortLabel() != null) { + data.putString("title", info.getShortLabel().toString()); + } + + if (info.getLongLabel() != null) { + data.putString("description", info.getLongLabel().toString()); + } + + + if (!Objects.requireNonNull(info.getCategories()).isEmpty()) { + if (info.getCategories().contains("note")) { + data.putString("type", "note"); + } else if (info.getCategories().contains("notebook")) { + data.putString("type", "notebook"); + } else if (info.getCategories().contains("tag")) { + data.putString("type", "tag"); + } + } + shortcuts.pushMap(data); + } + promise.resolve(shortcuts); + } + private Icon createLetterIcon(String type, String title) { String letter = type.contains("tag") ? "#" : title != null && !title.isEmpty() diff --git a/apps/mobile/app/hooks/use-actions.tsx b/apps/mobile/app/hooks/use-actions.tsx index f52ad205e..d5962b9c8 100644 --- a/apps/mobile/app/hooks/use-actions.tsx +++ b/apps/mobile/app/hooks/use-actions.tsx @@ -300,7 +300,6 @@ export const useActions = ({ id: item.id, title: value }); - eSendEvent(Navigation.routeNames.TaggedNotes); InteractionManager.runAfterInteractions(() => { useTagStore.getState().refresh(); @@ -1199,13 +1198,14 @@ export const useActions = ({ title: strings.addToHome(), icon: "cellphone-arrow-down", onPress: async () => { - const added = await NotesnookModule.addShortcut( - item.id, - item.type, - item.title, - (item as Note).headline || (item as Notebook).description || "" - ); - console.log(added); + try { + await NotesnookModule.addShortcut( + item.id, + item.type, + item.title, + (item as Note).headline || (item as Notebook).description || "" + ); + } catch (e) {} } }); } diff --git a/apps/mobile/app/stores/index.ts b/apps/mobile/app/stores/index.ts index 9171be95d..a1280e8c3 100644 --- a/apps/mobile/app/stores/index.ts +++ b/apps/mobile/app/stores/index.ts @@ -17,12 +17,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { db } from "../common/database"; +import { DatabaseLogger, db } from "../common/database"; import { eSendEvent } from "../services/event-manager"; import Navigation from "../services/navigation"; import { NotePreviewWidget } from "../services/note-preview-widget"; import Notifications from "../services/notifications"; import { eAfterSync } from "../utils/events"; +import { NotesnookModule, ShortcutInfo } from "../utils/notesnook-module"; import { useFavoriteStore } from "./use-favorite-store"; import { useMenuStore } from "./use-menu-store"; import { useMonographStore } from "./use-monograph-store"; @@ -34,6 +35,58 @@ import { useTagStore } from "./use-tag-store"; import { useTrashStore } from "./use-trash-store"; import { useUserStore } from "./use-user-store"; +async function syncShortcuts(result: ShortcutInfo[]) { + try { + for (let shortcut of result) { + switch (shortcut.type) { + case "note": + { + const note = await db.notes.note(shortcut.id); + if (!note) { + NotesnookModule.removeShortcut(shortcut.id); + } else if (note.title !== shortcut.title) { + NotesnookModule.updateShortcut( + shortcut.id, + note.title, + note.headline + ); + } + } + break; + case "notebook": + { + const notebook = await db.notebooks.notebook(shortcut.id); + if (!notebook) { + NotesnookModule.removeShortcut(shortcut.id); + } else if (notebook.title !== shortcut.title) { + NotesnookModule.updateShortcut( + shortcut.id, + notebook.title, + notebook.description + ); + } + } + break; + case "tag": + { + const tag = await db.tags.tag(shortcut.id); + if (!tag) { + NotesnookModule.removeShortcut(shortcut.id); + } else if (tag.title !== shortcut.title) { + NotesnookModule.updateShortcut(shortcut.id, tag.title, tag.title); + } + } + break; + } + } + } catch (e) { + DatabaseLogger.error( + e as Error, + "Error while syncing homescreen shortcuts" + ); + } +} + export function initAfterSync(type: "full" | "send" = "send") { if (type === "full") { Navigation.queueRoutesForUpdate(); @@ -46,9 +99,11 @@ export function initAfterSync(type: "full" | "send" = "send") { profile: db.settings.getProfile() }); } + Notifications.setupReminders(true); NotePreviewWidget.updateNotes(); eSendEvent(eAfterSync); + NotesnookModule.getAllShortcuts().then(syncShortcuts); } export async function initialize() {} diff --git a/apps/mobile/app/utils/notesnook-module.ts b/apps/mobile/app/utils/notesnook-module.ts index aa337f35e..9aac19f41 100644 --- a/apps/mobile/app/utils/notesnook-module.ts +++ b/apps/mobile/app/utils/notesnook-module.ts @@ -19,6 +19,13 @@ along with this program. If not, see . import { NativeModules, Platform } from "react-native"; +export type ShortcutInfo = { + id: string; + title: string; + description: string; + type: "note" | "notebook" | "tag"; +}; + interface NotesnookModuleInterface { getActivityName: () => Promise; setBackgroundColor: (color: string) => void; @@ -54,6 +61,7 @@ interface NotesnookModuleInterface { description?: string ) => Promise; removeAllShortcuts: () => Promise; + getAllShortcuts: () => Promise; } export const NotesnookModule: NotesnookModuleInterface = Platform.select({ @@ -78,7 +86,8 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({ addShortcut: () => Promise.resolve(false), removeShortcut: () => Promise.resolve(false), updateShortcut: () => Promise.resolve(false), - removeAllShortcuts: () => Promise.resolve(false) + removeAllShortcuts: () => Promise.resolve(false), + getAllShortcuts: () => Promise.resolve([]) }, android: NativeModules.NNativeModule }); From ca8e505cd03c1c56aa1fa281024147cf32737b1e Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 24 Dec 2025 14:17:52 +0500 Subject: [PATCH 4/6] mobile: add support for color shortcuts on homescreen --- .../notesnook/RCTNNativeModule.java | 42 +++++++++++-------- apps/mobile/app/hooks/use-actions.tsx | 9 +++- apps/mobile/app/hooks/use-app-events.tsx | 16 +++++++ .../app/navigation/navigation-stack.tsx | 12 ++++++ apps/mobile/app/stores/index.ts | 25 ++++++++++- apps/mobile/app/utils/notesnook-module.ts | 11 +++-- 6 files changed, 91 insertions(+), 24 deletions(-) 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 a32e85852..178fe31ab 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 @@ -246,7 +246,7 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { } @ReactMethod - public void addShortcut(final String id, final String type, final String title, final String description, Promise promise) { + public void addShortcut(final String id, final String type, final String title, final String description, final String color, Promise promise) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher"); return; @@ -263,7 +263,7 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { Intent intent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(uri)); intent.setPackage(mContext.getPackageName()); - Icon icon = createLetterIcon(type, title); + Icon icon = createLetterIcon(type, title, color); ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, id) .setShortLabel(title) .setLongLabel(description != null && !description.isEmpty() ? description : title) @@ -301,7 +301,7 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { } @ReactMethod - public void updateShortcut(final String id, final String title, final String description, Promise promise) { + public void updateShortcut(final String id, final String type, final String title, final String description,final String color, Promise promise) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher"); return; @@ -327,9 +327,10 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { if (existingShortcut == null) { return; } - + Icon icon = createLetterIcon(type, title, color); ShortcutInfo updatedShortcut = new ShortcutInfo.Builder(mContext, id) .setShortLabel(title) + .setIcon(icon) .setLongLabel(description != null && !description.isEmpty() ? description : title) .setIntent(existingShortcut.getIntent()) .build(); @@ -397,6 +398,8 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { data.putString("type", "notebook"); } else if (info.getCategories().contains("tag")) { data.putString("type", "tag"); + } else if (info.getCategories().contains("color")) { + data.putString("type", "color"); } } shortcuts.pushMap(data); @@ -404,12 +407,12 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { promise.resolve(shortcuts); } - private Icon createLetterIcon(String type, String title) { + private Icon createLetterIcon(String type, String title, String colorCode) { String letter = type.contains("tag") ? "#" : title != null && !title.isEmpty() ? title.substring(0, 1).toUpperCase() : "?"; - int color = getColorForLetter(letter); + int color = type.equals("color") ? Color.parseColor(colorCode) : getColorForLetter(letter); if (type.equals("notebook")) return Icon.createWithResource(mContext, R.drawable.ic_notebook); // Use a larger canvas and fill it completely to avoid white borders from launcher masking. @@ -417,19 +420,24 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { Bitmap bitmap = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); + if (type.equals("color")) { + Paint backgroundPaint = new Paint(); + backgroundPaint.setColor(color); + backgroundPaint.setAntiAlias(true); + canvas.drawCircle(iconSize / 2, iconSize / 2, iconSize/2, backgroundPaint); + } else { + Paint textPaint = new Paint(); + textPaint.setColor(color); + textPaint.setTextSize(130); + textPaint.setAntiAlias(true); + textPaint.setTextAlign(Paint.Align.CENTER); + textPaint.setTypeface(android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD)); + float x = iconSize / 2f; + float y = (iconSize / 2f) - ((textPaint.descent() + textPaint.ascent()) / 2f); - Paint textPaint = new Paint(); - textPaint.setColor(color); - textPaint.setTextSize(130); - textPaint.setAntiAlias(true); - textPaint.setTextAlign(Paint.Align.CENTER); - textPaint.setTypeface(android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD)); - - float x = iconSize / 2f; - float y = (iconSize / 2f) - ((textPaint.descent() + textPaint.ascent()) / 2f); - - canvas.drawText(letter, x, y, textPaint); + canvas.drawText(letter, x, y, textPaint); + } return Icon.createWithBitmap(bitmap); } diff --git a/apps/mobile/app/hooks/use-actions.tsx b/apps/mobile/app/hooks/use-actions.tsx index d5962b9c8..d74b89569 100644 --- a/apps/mobile/app/hooks/use-actions.tsx +++ b/apps/mobile/app/hooks/use-actions.tsx @@ -19,6 +19,7 @@ along with this program. If not, see . /* eslint-disable no-inner-declarations */ import { useAreFeaturesAvailable } from "@notesnook/common"; import { + Color, createInternalLink, Item, ItemReference, @@ -1191,7 +1192,10 @@ export const useActions = ({ if ( Platform.OS === "android" && - (item.type === "tag" || item.type === "note" || item.type === "notebook") + (item.type === "tag" || + item.type === "note" || + item.type === "notebook" || + item.type === "color") ) { actions.push({ id: "launcher-shortcut", @@ -1203,7 +1207,8 @@ export const useActions = ({ item.id, item.type, item.title, - (item as Note).headline || (item as Notebook).description || "" + (item as Note).headline || (item as Notebook).description || "", + (item as Color).colorCode ); } catch (e) {} } diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index 7c572240d..3a13f5414 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -216,6 +216,22 @@ const onAppOpenedFromURL = async (event: { }); } } + } else if ( + url.startsWith("https://app.notesnook.com/open_color?") && + !event.isInitialUrl + ) { + const id = new URL(url).searchParams.get("id"); + if (id) { + const color = await db.colors.color(id); + if (color) { + Navigation.navigate("ColoredNotes", { + type: "color", + id: color.id, + item: color, + canGoBack: true + }); + } + } } else if (url.startsWith("https://app.notesnook.com/open_reminder")) { const id = new URL(url).searchParams.get("id"); if (id) { diff --git a/apps/mobile/app/navigation/navigation-stack.tsx b/apps/mobile/app/navigation/navigation-stack.tsx index 9884182ee..48fa476c4 100644 --- a/apps/mobile/app/navigation/navigation-stack.tsx +++ b/apps/mobile/app/navigation/navigation-stack.tsx @@ -87,6 +87,18 @@ const AppNavigation = React.memo( }); return; } + } else if (url?.startsWith("https://app.notesnook.com/open_color?")) { + const id = new URL(url).searchParams.get("id"); + if (id) { + setHome({ + name: "ColoredNotes", + params: { + type: "color", + id: id + } + }); + return; + } } } diff --git a/apps/mobile/app/stores/index.ts b/apps/mobile/app/stores/index.ts index a1280e8c3..e2f0218f7 100644 --- a/apps/mobile/app/stores/index.ts +++ b/apps/mobile/app/stores/index.ts @@ -47,6 +47,7 @@ async function syncShortcuts(result: ShortcutInfo[]) { } else if (note.title !== shortcut.title) { NotesnookModule.updateShortcut( shortcut.id, + "note", note.title, note.headline ); @@ -61,6 +62,7 @@ async function syncShortcuts(result: ShortcutInfo[]) { } else if (notebook.title !== shortcut.title) { NotesnookModule.updateShortcut( shortcut.id, + "notebook", notebook.title, notebook.description ); @@ -73,7 +75,28 @@ async function syncShortcuts(result: ShortcutInfo[]) { if (!tag) { NotesnookModule.removeShortcut(shortcut.id); } else if (tag.title !== shortcut.title) { - NotesnookModule.updateShortcut(shortcut.id, tag.title, tag.title); + NotesnookModule.updateShortcut( + shortcut.id, + "tag", + tag.title, + tag.title + ); + } + } + break; + case "color": + { + const color = await db.colors.color(shortcut.id); + if (!color) { + NotesnookModule.removeShortcut(shortcut.id); + } else if (color.title !== shortcut.title) { + NotesnookModule.updateShortcut( + shortcut.id, + "color", + color.title, + color.title, + color.colorCode + ); } } break; diff --git a/apps/mobile/app/utils/notesnook-module.ts b/apps/mobile/app/utils/notesnook-module.ts index 9aac19f41..47f7783a8 100644 --- a/apps/mobile/app/utils/notesnook-module.ts +++ b/apps/mobile/app/utils/notesnook-module.ts @@ -23,7 +23,7 @@ export type ShortcutInfo = { id: string; title: string; description: string; - type: "note" | "notebook" | "tag"; + type: "note" | "notebook" | "tag" | "color"; }; interface NotesnookModuleInterface { @@ -50,15 +50,18 @@ interface NotesnookModuleInterface { isGestureNavigationEnabled: () => boolean; addShortcut: ( id: string, - type: "note" | "notebook" | "tag", + type: "note" | "notebook" | "tag" | "color", title: string, - description?: string + description?: string, + color?: string ) => Promise; removeShortcut: (id: string) => Promise; updateShortcut: ( id: string, + type: "note" | "notebook" | "tag" | "color", title: string, - description?: string + description?: string, + color?: string ) => Promise; removeAllShortcuts: () => Promise; getAllShortcuts: () => Promise; From 873b180dabbdf816f3a0ca04a28bce59010b686a Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 25 Dec 2025 11:37:27 +0500 Subject: [PATCH 5/6] mobile: enable feature for pro users --- apps/mobile/app/hooks/use-actions.tsx | 17 ++++++++++++++++- .../common/src/utils/is-feature-available.ts | 11 +++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/mobile/app/hooks/use-actions.tsx b/apps/mobile/app/hooks/use-actions.tsx index d74b89569..090f96088 100644 --- a/apps/mobile/app/hooks/use-actions.tsx +++ b/apps/mobile/app/hooks/use-actions.tsx @@ -162,7 +162,8 @@ export const useActions = ({ "shortcuts", "notebooks", "customizableSidebar", - "customHomepage" + "customHomepage", + "androidLauncherShortcuts" ]); const [item, setItem] = useState(propItem); const { colors } = useThemeColors(); @@ -1201,7 +1202,21 @@ export const useActions = ({ id: "launcher-shortcut", title: strings.addToHome(), icon: "cellphone-arrow-down", + locked: !features?.androidLauncherShortcuts.isAllowed, onPress: async () => { + if (features && !features?.androidLauncherShortcuts.isAllowed) { + ToastManager.show({ + message: features?.androidLauncherShortcuts.error, + type: "info", + actionText: strings.upgrade(), + context: "local", + func: () => { + PaywallSheet.present(features?.androidLauncherShortcuts); + } + }); + return; + } + try { await NotesnookModule.addShortcut( item.id, diff --git a/packages/common/src/utils/is-feature-available.ts b/packages/common/src/utils/is-feature-available.ts index 27e0cd6f0..b30df8136 100644 --- a/packages/common/src/utils/is-feature-available.ts +++ b/packages/common/src/utils/is-feature-available.ts @@ -469,6 +469,17 @@ const features = { believer: createLimit(true), legacyPro: createLimit(true) } + }), + androidLauncherShortcuts: createFeature({ + id: "androidLauncherShortcuts", + title: "Android Launcher Shortcuts", + availability: { + free: createLimit(false), + essential: createLimit(false), + pro: createLimit(true), + believer: createLimit(true), + legacyPro: createLimit(true) + } }) }; From 22497199c512589ed3d162ef10ac0dedcd0316aa Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 25 Dec 2025 11:40:05 +0500 Subject: [PATCH 6/6] mobile: catch errors --- apps/mobile/app/stores/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/mobile/app/stores/index.ts b/apps/mobile/app/stores/index.ts index e2f0218f7..895b798eb 100644 --- a/apps/mobile/app/stores/index.ts +++ b/apps/mobile/app/stores/index.ts @@ -126,7 +126,12 @@ export function initAfterSync(type: "full" | "send" = "send") { Notifications.setupReminders(true); NotePreviewWidget.updateNotes(); eSendEvent(eAfterSync); - NotesnookModule.getAllShortcuts().then(syncShortcuts); + + NotesnookModule.getAllShortcuts() + .then(syncShortcuts) + .catch((e) => { + DatabaseLogger.log(e); + }); } export async function initialize() {}