diff --git a/.gitignore b/.gitignore
index f9070ace..c54f8060 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,5 @@ web-build/
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
-ios/
\ No newline at end of file
+ios/
+android/
\ No newline at end of file
diff --git a/app.config.ts b/app.config.ts
index 8cc7ce56..383806fb 100644
--- a/app.config.ts
+++ b/app.config.ts
@@ -9,9 +9,7 @@ module.exports = {
slug: "hydra",
version: packageJson.version,
newArchEnabled: true,
- runtimeVersion: {
- policy: 'appVersion',
- },
+ runtimeVersion: packageJson.version,
orientation: "portrait",
icon: "./assets/images/icon.png",
scheme: "hydra",
@@ -33,6 +31,7 @@ module.exports = {
},
},
android: {
+ package: "com.dmilin.hydra",
adaptiveIcon: {
foregroundImage: "./assets/images/adaptive-icon.png",
backgroundColor: "#000000"
diff --git a/app/index.tsx b/app/index.tsx
index 021084a1..04edea70 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -11,7 +11,8 @@ import { registerRootComponent } from "expo";
import { useFonts } from "expo-font";
import { SplashScreen } from "expo-router";
import React, { useEffect, useState } from "react";
-import { AppState, LogBox } from "react-native";
+import { AppState, LogBox, Platform } from "react-native";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { enableFreeze } from "react-native-screens";
@@ -59,7 +60,9 @@ Sentry.init({
SplashScreen.preventAutoHideAsync();
// Tells non visble tabs and screens to not rerender in the background
-enableFreeze(true);
+if (Platform.OS !== "android") {
+ enableFreeze(true);
+}
function RootLayout() {
const { success: migrationsComplete, error } = useMigrations(db, migrations);
@@ -105,34 +108,36 @@ function RootLayout() {
migrationsComplete &&
fontsLoaded &&
dbMaintenanceDone && (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
);
}
diff --git a/components/Modals/QuickSubredditSearch.tsx b/components/Modals/QuickSubredditSearch.tsx
index fda037da..cdf019ae 100644
--- a/components/Modals/QuickSubredditSearch.tsx
+++ b/components/Modals/QuickSubredditSearch.tsx
@@ -99,6 +99,7 @@ export default function QuickSubredditSearch({
return (
-
+ {isActionSheetShowing && (
+
+ )}
{children}
);
diff --git a/contexts/ModalContext.tsx b/contexts/ModalContext.tsx
index 2f2ab6ed..94ac4675 100644
--- a/contexts/ModalContext.tsx
+++ b/contexts/ModalContext.tsx
@@ -1,12 +1,5 @@
-import {
- ReactNode,
- createContext,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
-import { Animated, StyleSheet, useWindowDimensions } from "react-native";
+import { ReactNode, createContext, useMemo, useState } from "react";
+import { StyleSheet, View } from "react-native";
type ModalContextType = {
setModal: (modal?: ReactNode) => void;
@@ -20,16 +13,6 @@ export const ModalContext = createContext(initialModalContext);
export function ModalProvider({ children }: React.PropsWithChildren) {
const [modal, setModal] = useState(null);
- const { height } = useWindowDimensions();
- const modalPosition = useRef(new Animated.Value(height)).current;
-
- useEffect(() => {
- Animated.spring(modalPosition, {
- toValue: modal ? 0 : height,
- bounciness: 2,
- useNativeDriver: true,
- }).start();
- }, [modal]);
/**
* Since this provider only provides functions, we need to memoize the value
@@ -44,28 +27,19 @@ export function ModalProvider({ children }: React.PropsWithChildren) {
return (
-
- {modal}
-
{children}
+ {modal && {modal}}
);
}
const styles = StyleSheet.create({
modalContainer: {
- position: "relative",
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
zIndex: 1000,
},
});
diff --git a/contexts/StackFutureContext.tsx b/contexts/StackFutureContext.tsx
index 35a79aff..514c80b4 100644
--- a/contexts/StackFutureContext.tsx
+++ b/contexts/StackFutureContext.tsx
@@ -16,7 +16,7 @@ import {
useEffect,
useRef,
} from "react";
-import { Animated, useWindowDimensions } from "react-native";
+import { Animated, Platform, useWindowDimensions } from "react-native";
import { StackParamsList } from "../app/stack";
@@ -66,46 +66,50 @@ export function StackFutureProvider({
clearFuture: () => (futureRoutes.current = []),
}}
>
- {
- gestureStart.current = {
- x: e.nativeEvent.pageX,
- y: e.nativeEvent.pageY,
- };
- return false;
- }}
- onMoveShouldSetResponder={() =>
- !!gestureStart.current &&
- width - gestureStart.current.x < 30 &&
- futureRoutes.current.length > 0
- }
- onResponderMove={(e) => {
- if (!gestureStart.current) return false;
- const deltaX = gestureStart.current.x - e.nativeEvent.pageX;
- const deltaY = e.nativeEvent.pageY - gestureStart.current.y;
- const angleDegrees = Math.abs(
- Math.atan2(deltaY, deltaX) * (180 / Math.PI),
- );
- if (angleDegrees > 15 && deltaY > 30) {
- gestureStart.current = null;
+ {Platform.OS === "android" ? (
+ children
+ ) : (
+ {
+ gestureStart.current = {
+ x: e.nativeEvent.pageX,
+ y: e.nativeEvent.pageY,
+ };
return false;
+ }}
+ onMoveShouldSetResponder={() =>
+ !!gestureStart.current &&
+ width - gestureStart.current.x < 30 &&
+ futureRoutes.current.length > 0
}
- if (deltaX > 15 && angleDegrees < 15) {
- const popped = futureRoutes.current.pop();
- if (!popped) return false;
- navigation.push(popped.name as any, popped.params as any);
- gestureStart.current = null;
- return true;
- }
- return false;
- }}
- >
- {children}
-
+ onResponderMove={(e) => {
+ if (!gestureStart.current) return false;
+ const deltaX = gestureStart.current.x - e.nativeEvent.pageX;
+ const deltaY = e.nativeEvent.pageY - gestureStart.current.y;
+ const angleDegrees = Math.abs(
+ Math.atan2(deltaY, deltaX) * (180 / Math.PI),
+ );
+ if (angleDegrees > 15 && deltaY > 30) {
+ gestureStart.current = null;
+ return false;
+ }
+ if (deltaX > 15 && angleDegrees < 15) {
+ const popped = futureRoutes.current.pop();
+ if (!popped) return false;
+ navigation.push(popped.name as any, popped.params as any);
+ gestureStart.current = null;
+ return true;
+ }
+ return false;
+ }}
+ >
+ {children}
+
+ )}
);
}
diff --git a/package-lock.json b/package-lock.json
index 516cd0a1..7567c504 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "hydra",
- "version": "3.1.0",
+ "version": "3.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hydra",
- "version": "3.1.0",
+ "version": "3.3.0",
"dependencies": {
"@expo/react-native-action-sheet": "^4.1.1",
"@expo/vector-icons": "^15.0.3",
@@ -50,10 +50,11 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
- "react-native-mmkv": "^3.3.0",
+ "react-native-gesture-handler": "~2.28.0",
+ "react-native-mmkv": "3.3.3",
"react-native-purchases": "^8.9.3",
"react-native-safe-area-context": "5.6.0",
- "react-native-screens": "4.19.0-nightly-20251205-910978b9a",
+ "react-native-screens": "4.24.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-web": "^0.21.0",
"react-native-webview": "13.15.0",
@@ -1581,6 +1582,18 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/@egjs/hammerjs": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
+ "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hammerjs": "^2.0.36"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/@esbuild-kit/core-utils": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
@@ -4137,6 +4150,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/hammerjs": {
+ "version": "2.0.46",
+ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
+ "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
+ "license": "MIT"
+ },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
@@ -12355,6 +12374,21 @@
}
}
},
+ "node_modules/react-native-gesture-handler": {
+ "version": "2.28.0",
+ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz",
+ "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@egjs/hammerjs": "^2.0.17",
+ "hoist-non-react-statics": "^3.3.0",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-is-edge-to-edge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
@@ -12366,9 +12400,9 @@
}
},
"node_modules/react-native-mmkv": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-3.3.0.tgz",
- "integrity": "sha512-2iPjIJ+IAODXN35wm53EN6nv+hR/0NXLLiLWOdPA/0gXDB2dYVrr6MqvoiFpxhLhdj0B8fkf5ARNVavJaSvuuQ==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-3.3.3.tgz",
+ "integrity": "sha512-GMsfOmNzx0p5+CtrCFRVtpOOMYNJXuksBVARSQrCFaZwjUyHJdQzcN900GGaFFNTxw2fs8s5Xje//RDKj9+PZA==",
"license": "(MIT AND BSD-3-Clause)",
"peerDependencies": {
"react": "*",
@@ -12403,9 +12437,9 @@
}
},
"node_modules/react-native-screens": {
- "version": "4.19.0-nightly-20251205-910978b9a",
- "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.19.0-nightly-20251205-910978b9a.tgz",
- "integrity": "sha512-5q3cpW1xqulUqVTJQsF/thPkoh7S6egxvRapypKWEVqwYr5FKIIakzjaGdtIlZ8o925XYtoUAk1qnDgBXM/2TA==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.24.0.tgz",
+ "integrity": "sha512-SyoiGaDofiyGPFrUkn1oGsAzkRuX1JUvTD9YQQK3G1JGQ5VWkvHgYSsc1K9OrLsDQxN7NmV71O0sHCAh8cBetA==",
"license": "MIT",
"dependencies": {
"react-freeze": "^1.0.0",
diff --git a/package.json b/package.json
index 5754d312..a50cae5b 100644
--- a/package.json
+++ b/package.json
@@ -58,10 +58,11 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
- "react-native-mmkv": "^3.3.0",
+ "react-native-gesture-handler": "~2.28.0",
+ "react-native-mmkv": "3.3.3",
"react-native-purchases": "^8.9.3",
"react-native-safe-area-context": "5.6.0",
- "react-native-screens": "4.19.0-nightly-20251205-910978b9a",
+ "react-native-screens": "4.24.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-web": "^0.21.0",
"react-native-webview": "13.15.0",
@@ -83,7 +84,7 @@
"wrapper-webpack-plugin": "^2.2.2"
},
"overrides": {
- "react-native-screens": "4.19.0-nightly-20251205-910978b9a"
+ "react-native-screens": "4.24.0"
},
"private": true
-}
\ No newline at end of file
+}