From 9d9c3b7db5dddb307a0bc8e3a1140e7d07dff2fd Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Tue, 30 Dec 2025 14:23:39 +0500 Subject: [PATCH] mobile: add simple tab bar implementation This fixes most bugs faced by users in side-menu on ios like swipe gesture working when it is disabled and navigation screen stuck on older ios versions. --- .../mobile/app/components/side-menu/index.tsx | 123 ++++++++++++++---- .../components/side-menu/side-menu-home.tsx | 1 + .../ios/Notesnook.xcodeproj/project.pbxproj | 10 +- apps/mobile/ios/Podfile.lock | 52 ++------ apps/mobile/package-lock.json | 34 +---- apps/mobile/package.json | 4 +- 6 files changed, 123 insertions(+), 101 deletions(-) diff --git a/apps/mobile/app/components/side-menu/index.tsx b/apps/mobile/app/components/side-menu/index.tsx index d95d731f1..a4972a38e 100644 --- a/apps/mobile/app/components/side-menu/index.tsx +++ b/apps/mobile/app/components/side-menu/index.tsx @@ -21,15 +21,6 @@ import { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; import React from "react"; import { View } from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; -import { - NavigationState, - Route, - SceneMap, - SceneRendererProps, - TabDescriptor, - TabView -} from "react-native-tab-view"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { db } from "../../common/database"; import { useGroupOptions } from "../../hooks/use-group-options"; @@ -59,7 +50,104 @@ import SettingsService from "../../services/settings"; import { isFeatureAvailable } from "@notesnook/common"; import PaywallSheet from "../sheets/paywall"; import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; -const renderScene = SceneMap({ + +/** + * Simple Tab View Implementation for the Side bar + */ +type SimpleRoute = { + key: string; + title?: string; +}; + +type SimpleNavigationState = { + index: number; + routes: SimpleRoute[]; +}; + +type SimpleTabBarProps = { + navigationState: SimpleNavigationState; + jumpTo: (key: string) => void; +}; + +type SimpleTabViewProps = { + navigationState: SimpleNavigationState; + renderScene: ({ route }: { route: SimpleRoute }) => React.ReactNode; + renderTabBar?: (props: SimpleTabBarProps) => React.ReactNode; + onIndexChange?: (index: number) => void; +}; + +const createSceneMap = ( + scenes: Record> +): ((props: { route: SimpleRoute }) => React.ReactNode) => { + return ({ route }: { route: SimpleRoute }) => { + const SceneComponent = scenes[route.key]; + if (!SceneComponent) return null; + return ; + }; +}; + +const SimpleTabView = ({ + navigationState, + renderScene, + renderTabBar, + onIndexChange +}: SimpleTabViewProps) => { + const loadedKeysRef = React.useRef(new Set()); + const scenesRef = React.useRef(new Map()); + const jumpTo = React.useCallback( + (key: string) => { + const nextIndex = navigationState.routes.findIndex( + (route) => route.key === key + ); + if (nextIndex !== -1 && nextIndex !== navigationState.index) { + onIndexChange?.(nextIndex); + } + }, + [navigationState.index, navigationState.routes, onIndexChange] + ); + + const activeKey = navigationState.routes[navigationState.index]?.key; + if (activeKey && !loadedKeysRef.current.has(activeKey)) { + loadedKeysRef.current.add(activeKey); + } + + const getSceneForRoute = React.useCallback( + (route: SimpleRoute) => { + const cached = scenesRef.current.get(route.key); + if (cached) return cached; + const created = renderScene({ route }); + scenesRef.current.set(route.key, created); + return created; + }, + [renderScene] + ); + + return ( + + + {navigationState.routes.map((route, routeIndex) => ( + + {loadedKeysRef.current.has(route.key) + ? getSceneForRoute(route) + : navigationState.index === routeIndex + ? getSceneForRoute(route) + : null} + + ))} + + + {renderTabBar ? renderTabBar({ navigationState, jumpTo }) : null} + + ); +}; + +const renderScene = createSceneMap({ home: SideMenuHome, notebooks: SideMenuNotebooks, tags: SideMenuTags, @@ -73,7 +161,7 @@ export const SideMenu = React.memo( const [index, setIndex] = React.useState( SettingsService.getProperty("defaultSidebarTab") ); - const [routes] = React.useState([ + const [routes] = React.useState([ { key: "home", title: "Home" @@ -98,15 +186,11 @@ export const SideMenu = React.memo( paddingLeft: insets.left }} > - } - tabBarPosition="bottom" renderScene={renderScene} onIndexChange={setIndex} - swipeEnabled={false} - animationEnabled={false} - lazy /> ); @@ -114,12 +198,7 @@ export const SideMenu = React.memo( () => true ); -const TabBar = ( - props: SceneRendererProps & { - navigationState: NavigationState; - options: Record> | undefined; - } -) => { +const TabBar = (props: SimpleTabBarProps) => { const dragging = useSideBarDraggingStore((state) => state.dragging); const { colors, isDark } = useThemeColors(); const groupOptions = useGroupOptions( diff --git a/apps/mobile/app/components/side-menu/side-menu-home.tsx b/apps/mobile/app/components/side-menu/side-menu-home.tsx index b8b78efcb..efee88c87 100644 --- a/apps/mobile/app/components/side-menu/side-menu-home.tsx +++ b/apps/mobile/app/components/side-menu/side-menu-home.tsx @@ -85,6 +85,7 @@ export function SideMenuHome() { keyboardDismissMode="interactive" keyboardShouldPersistTaps="handled" keyExtractor={() => "scroll-items"} + bounces={false} renderItem={() => ( <> 1.0) - - Yoga - react-native-pdf (7.0.3): - boost - DoubleConversion @@ -3062,10 +3033,10 @@ PODS: - Yoga - RNKeychain (4.0.5): - React - - RNNotifee (7.4.10): + - RNNotifee (7.4.11): - React-Core - - RNNotifee/NotifeeCore (= 7.4.10) - - RNNotifee/NotifeeCore (7.4.10): + - RNNotifee/NotifeeCore (= 7.4.11) + - RNNotifee/NotifeeCore (7.4.11): - React-Core - RNPrivacySnapshot (1.0.0): - React-Core @@ -3161,7 +3132,7 @@ PODS: - RNWorklets - SocketRocket - Yoga - - RNScreens (4.18.0): + - RNScreens (4.19.0): - boost - DoubleConversion - fast_float @@ -3188,10 +3159,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 4.18.0) + - RNScreens/common (= 4.19.0) - SocketRocket - Yoga - - RNScreens/common (4.18.0): + - RNScreens/common (4.19.0): - boost - DoubleConversion - fast_float @@ -3414,7 +3385,6 @@ PODS: - pop (~> 1.0) - SocketRocket (0.7.1) - SSZipArchive (2.4.3) - - SwiftUIIntrospect (1.3.0) - SwiftyRSA (1.7.0): - SwiftyRSA/ObjC (= 1.7.0) - SwiftyRSA/ObjC (1.7.0) @@ -3486,7 +3456,6 @@ DEPENDENCIES: - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-notification-sounds (from `../node_modules/react-native-notification-sounds`) - react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`) - - react-native-pager-view (from `../node_modules/react-native-pager-view`) - react-native-pdf (from `../node_modules/react-native-pdf`) - react-native-quick-sqlite (from `../node_modules/react-native-quick-sqlite`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) @@ -3570,7 +3539,6 @@ SPEC REPOS: - SDWebImage - SocketRocket - SSZipArchive - - SwiftUIIntrospect - SwiftyRSA - TOCropViewController @@ -3694,8 +3662,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-notification-sounds" react-native-orientation-locker: :path: "../node_modules/react-native-orientation-locker" - react-native-pager-view: - :path: "../node_modules/react-native-pager-view" react-native-pdf: :path: "../node_modules/react-native-pdf" react-native-quick-sqlite: @@ -3906,7 +3872,6 @@ SPEC CHECKSUMS: react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 react-native-notification-sounds: ce106d58df0dd384bccbd2e84fb53accab7cc068 react-native-orientation-locker: cc6f357b289a2e0dd2210fea0c52cb8e0727fdaa - react-native-pager-view: d7d2aa47f54343bf55fdcee3973503dd27c2bd37 react-native-pdf: edc236298f13f1609e42d41e45b8b6ea88ed10f9 react-native-quick-sqlite: 1ed8d3db1e22a8604d006be69f06053382e93bb0 react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616 @@ -3962,10 +3927,10 @@ SPEC CHECKSUMS: RNIap: 61f183ac917792fae42b0326b1bef33598c1adf6 RNImageCropPicker: 5fd4ceaead64d8c53c787e4e559004f97bc76df7 RNKeychain: ffd0513e676445c637410b47249460cbf56bc9cb - RNNotifee: 3840cc81add9954a5dec040f96442498f3f95d7b + RNNotifee: c4827fa2cec60ab634d62bafdb5cf57cd2999d54 RNPrivacySnapshot: ccad3a548338c2f526bb7b1789af3fb0618b7d1d RNReanimated: f1868b36f4b2b52a0ed00062cfda69506f75eaee - RNScreens: d821082c6dd1cb397cc0c98b026eeafaa68be479 + RNScreens: ffbb0296608eb3560de641a711bbdb663ed1f6b4 RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc RNShare: 5d39a36f2d3d9a6c9344bfb3232c8ed607924a35 RNSVG: 8c0bbfa480a24b24468f1c76bd852a4aac3178e6 @@ -3976,7 +3941,6 @@ SPEC CHECKSUMS: SexyTooltip: 5c9b4dec52bfb317938cb0488efd9da3717bb6fd SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef - SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6 TOCropViewController: 797deaf39c90e6e9ddd848d88817f6b9a8a09888 toolbar-android: c426ed5bd3dcccfed20fd79533efc0d1ae0ef018 diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index a8a365465..eb9092287 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -102,7 +102,6 @@ "react-native-navigation-bar-color": "2.0.2", "react-native-notification-sounds": "0.5.5", "react-native-orientation-locker": "^1.7.0", - "react-native-pager-view": "^8.0.0", "react-native-pdf": "^7.0.3", "react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot", "react-native-progress": "5.0.0", @@ -113,12 +112,11 @@ "react-native-safe-area-context": "^5.6.1", "react-native-scoped-storage": "^1.9.5", "react-native-screenguard": "1.0.0", - "react-native-screens": "^4.16.0", + "react-native-screens": "^4.19.0", "react-native-securerandom": "^1.0.1", "react-native-share": "^12.0.3", "react-native-svg": "^15.12.0", "react-native-swiper-flatlist": "3.2.2", - "react-native-tab-view": "^4.2.2", "react-native-theme-switch-animation": "^0.6.0", "react-native-tooltips": "^1.0.3", "react-native-url-polyfill": "^2.0.0", @@ -17742,16 +17740,6 @@ } } }, - "node_modules/react-native-pager-view": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-8.0.0.tgz", - "integrity": "sha512-oAwlWT1lhTkIs9HhODnjNNl/owxzn9DP1MbP+az6OTUdgbmzA16Up83sBH8NRKwrH8rNm7iuWnX1qMqiiWOLhg==", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-pdf": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-7.0.3.tgz", @@ -17900,9 +17888,9 @@ } }, "node_modules/react-native-screens": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.18.0.tgz", - "integrity": "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ==", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.19.0.tgz", + "integrity": "sha512-qSDAO3AL5bti0Ri7KZRSVmWlhDr8MV86N5GruiKVQfEL7Zx2nUi3Dl62lqHUAD/LnDvOPuDDsMHCfIpYSv3hPQ==", "license": "MIT", "dependencies": { "react-freeze": "^1.0.0", @@ -17958,20 +17946,6 @@ "react-native": ">=0.59.0" } }, - "node_modules/react-native-tab-view": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-4.2.2.tgz", - "integrity": "sha512-NXtrG6OchvbGjsvbySJGVocXxo4Y2vA17ph4rAaWtA2jh+AasD8OyikKBRg2SmllEfeQ+GEhcKe8kulHv8BhTg==", - "license": "MIT", - "dependencies": { - "use-latest-callback": "^0.2.4" - }, - "peerDependencies": { - "react": ">= 18.2.0", - "react-native": "*", - "react-native-pager-view": ">= 6.0.0" - } - }, "node_modules/react-native-theme-switch-animation": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/react-native-theme-switch-animation/-/react-native-theme-switch-animation-0.6.0.tgz", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 68ba6d40d..66f671ca3 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -118,7 +118,6 @@ "react-native-navigation-bar-color": "2.0.2", "react-native-notification-sounds": "0.5.5", "react-native-orientation-locker": "^1.7.0", - "react-native-pager-view": "^8.0.0", "react-native-pdf": "^7.0.3", "react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot", "react-native-progress": "5.0.0", @@ -129,12 +128,11 @@ "react-native-safe-area-context": "^5.6.1", "react-native-scoped-storage": "^1.9.5", "react-native-screenguard": "1.0.0", - "react-native-screens": "^4.16.0", + "react-native-screens": "^4.19.0", "react-native-securerandom": "^1.0.1", "react-native-share": "^12.0.3", "react-native-svg": "^15.12.0", "react-native-swiper-flatlist": "3.2.2", - "react-native-tab-view": "^4.2.2", "react-native-theme-switch-animation": "^0.6.0", "react-native-tooltips": "^1.0.3", "react-native-url-polyfill": "^2.0.0",