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.
This commit is contained in:
Ammar Ahmed
2025-12-30 14:23:39 +05:00
parent 8435e69cb5
commit 9d9c3b7db5
6 changed files with 123 additions and 101 deletions

View File

@@ -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<string, React.ComponentType<any>>
): ((props: { route: SimpleRoute }) => React.ReactNode) => {
return ({ route }: { route: SimpleRoute }) => {
const SceneComponent = scenes[route.key];
if (!SceneComponent) return null;
return <SceneComponent />;
};
};
const SimpleTabView = ({
navigationState,
renderScene,
renderTabBar,
onIndexChange
}: SimpleTabViewProps) => {
const loadedKeysRef = React.useRef(new Set<string>());
const scenesRef = React.useRef(new Map<string, React.ReactNode>());
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 (
<View style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
{navigationState.routes.map((route, routeIndex) => (
<View
key={route.key}
style={{
flex: 1,
display: navigationState.index === routeIndex ? "flex" : "none"
}}
>
{loadedKeysRef.current.has(route.key)
? getSceneForRoute(route)
: navigationState.index === routeIndex
? getSceneForRoute(route)
: null}
</View>
))}
</View>
{renderTabBar ? renderTabBar({ navigationState, jumpTo }) : null}
</View>
);
};
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<Route[]>([
const [routes] = React.useState<SimpleRoute[]>([
{
key: "home",
title: "Home"
@@ -98,15 +186,11 @@ export const SideMenu = React.memo(
paddingLeft: insets.left
}}
>
<TabView
<SimpleTabView
navigationState={{ index, routes }}
renderTabBar={(props) => <TabBar {...props} />}
tabBarPosition="bottom"
renderScene={renderScene}
onIndexChange={setIndex}
swipeEnabled={false}
animationEnabled={false}
lazy
/>
</View>
);
@@ -114,12 +198,7 @@ export const SideMenu = React.memo(
() => true
);
const TabBar = (
props: SceneRendererProps & {
navigationState: NavigationState<Route>;
options: Record<string, TabDescriptor<Route>> | undefined;
}
) => {
const TabBar = (props: SimpleTabBarProps) => {
const dragging = useSideBarDraggingStore((state) => state.dragging);
const { colors, isDark } = useThemeColors();
const groupOptions = useGroupOptions(

View File

@@ -85,6 +85,7 @@ export function SideMenuHome() {
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
keyExtractor={() => "scroll-items"}
bounces={false}
renderItem={() => (
<>
<ReorderableList

View File

@@ -1724,7 +1724,10 @@
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -1789,7 +1792,10 @@
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;

View File

@@ -2004,35 +2004,6 @@ PODS:
- React
- react-native-orientation-locker (1.7.0):
- React-Core
- react-native-pager-view (8.0.0):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- SwiftUIIntrospect (~> 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

View File

@@ -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",

View File

@@ -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",