diff --git a/app.config.ts b/app.config.ts
index 0cbfbc0c6..a8535d421 100644
--- a/app.config.ts
+++ b/app.config.ts
@@ -52,7 +52,7 @@ export default ({ config }: ConfigContext): ExpoConfig =>
owner: 'eten-genesis',
name: getAppName(appVariant),
slug: 'langquest',
- version: '2.0.1',
+ version: '2.0.3',
orientation: 'portrait',
icon: iconLight,
scheme: getScheme(appVariant),
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 7a8227980..e70c38d81 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -34,6 +34,7 @@ import { DarkTheme, DefaultTheme } from '@react-navigation/native';
import { StatusBar } from 'expo-status-bar';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
+import { KeyboardProvider } from 'react-native-keyboard-controller';
import {
configureReanimatedLogger,
ReanimatedLogLevel
@@ -136,15 +137,17 @@ export default function RootLayout() {
-
- {/* OTA Update Banner - shown before login and after */}
-
-
-
-
-
-
-
+
+
+ {/* OTA Update Banner - shown before login and after */}
+
+
+
+
+
+
+
+
diff --git a/components/AuthModal.tsx b/components/AuthModal.tsx
index afe444db9..941ce7be1 100644
--- a/components/AuthModal.tsx
+++ b/components/AuthModal.tsx
@@ -3,13 +3,7 @@ import { AuthNavigator } from '@/navigators/AuthNavigator';
import { useThemeColor } from '@/utils/styleUtils';
import { XIcon } from 'lucide-react-native';
import React from 'react';
-import {
- KeyboardAvoidingView,
- Modal,
- Platform,
- Pressable,
- View
-} from 'react-native';
+import { Modal, Pressable, View } from 'react-native';
import { Icon } from './ui/icon';
interface AuthModalProps {
@@ -32,24 +26,19 @@ export function AuthModal({
presentationStyle="pageSheet"
onRequestClose={onClose}
>
-
-
- {/* Close button */}
-
-
-
-
-
-
+
+ {/* Close button */}
+
+
+
+
-
+
+
);
}
diff --git a/components/NewReportModal.tsx b/components/NewReportModal.tsx
index e213e6b11..2b23fcc05 100644
--- a/components/NewReportModal.tsx
+++ b/components/NewReportModal.tsx
@@ -14,13 +14,15 @@ import { XIcon } from 'lucide-react-native';
import React, { useMemo, useState } from 'react';
import {
Alert,
- KeyboardAvoidingView,
Modal,
Pressable,
- ScrollView,
TouchableWithoutFeedback,
View
} from 'react-native';
+import {
+ KeyboardAwareScrollView,
+ KeyboardToolbar
+} from 'react-native-keyboard-controller';
interface ReportModalProps {
isVisible: boolean;
@@ -171,117 +173,119 @@ export const ReportModal: React.FC = ({
>
-
- e.stopPropagation()}>
-
-
- {modalTitle}
-
-
-
-
+ e.stopPropagation()}>
+
+
+ {modalTitle}
+
+
+
+
-
-
-
-
- {t('selectReasonLabel')}
-
-
- handleReasonSelect(
- value as (typeof reasonOptions)[number]
- )
- }
- >
- {reportReasons.map((option) => (
-
- ))}
-
-
+
+
+
+
+ {t('selectReasonLabel')}
+
+
+ handleReasonSelect(
+ value as (typeof reasonOptions)[number]
+ )
+ }
+ >
+ {reportReasons.map((option) => (
+
+ ))}
+
+
-
-
- {t('additionalDetails')}
-
-
-
+
+
+ {t('additionalDetails')}
+
+
+
- {/* Blocking options */}
-
-
- {t('options')}
-
- {/* Block content option - only for authenticated users */}
- {isAuthenticated ? (
- <>
+ {/* Blocking options */}
+
+
+ {t('options')}
+
+ {/* Block content option - only for authenticated users */}
+ {isAuthenticated ? (
+ <>
+
+
+
+
+
+ {creatorId && creatorId !== currentUser?.id && (
-
- {creatorId && creatorId !== currentUser?.id && (
-
-
-
-
- )}
- >
- ) : (
-
-
- {t('blockContentLoginMessage') ||
- 'We store information about what to block on your account. Please register to ensure blocked content can be properly hidden.'}
-
-
- )}
-
+ )}
+ >
+ ) : (
+
+
+ {t('blockContentLoginMessage') ||
+ 'We store information about what to block on your account. Please register to ensure blocked content can be properly hidden.'}
+
+
+ )}
-
+
+
-
-
-
-
+
+
+
+
+
+
);
};
diff --git a/components/ProjectMembershipModal.tsx b/components/ProjectMembershipModal.tsx
index bdff81999..398f08e8f 100644
--- a/components/ProjectMembershipModal.tsx
+++ b/components/ProjectMembershipModal.tsx
@@ -5,7 +5,7 @@ import { Checkbox } from '@/components/ui/checkbox';
import { Icon } from '@/components/ui/icon';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
-import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Text } from '@/components/ui/text';
import { useAuth } from '@/contexts/AuthContext';
import type { profile, request } from '@/db/drizzleSchema';
@@ -34,15 +34,8 @@ import {
XIcon
} from 'lucide-react-native';
import React, { useState } from 'react';
-import {
- Alert,
- KeyboardAvoidingView,
- Modal,
- Platform,
- Pressable,
- ScrollView,
- View
-} from 'react-native';
+import { Alert, Modal, Pressable, ScrollView, View } from 'react-native';
+import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
const MAX_INVITE_ATTEMPTS = 3;
@@ -1009,132 +1002,110 @@ export const ProjectMembershipModal: React.FC = ({
const isInviteButtonEnabled = inviteEmail.trim() && isValidEmail(inviteEmail);
return (
-
-
-
-
-
- {t('projectMembers')}
-
-
-
-
+
+
+ {t('projectMembers')}
+
+
+
+
-
- {projectLoading ? (
-
- {t('loadingProjectDetails')}
-
- ) : (
-
+ {projectLoading ? (
+
+ {t('loadingProjectDetails')}
+
+ ) : (
+
+ {/* Tabs */}
+
+ setActiveTab(value as 'members' | 'invited' | 'requests')
+ }
+ className="flex-1"
>
- {/* Tabs Header */}
-
- setActiveTab(value as 'members' | 'invited' | 'requests')
- }
- >
-
-
+
+
+
+ {t('members')} ({sortedMembers.length})
+
+
+
+
+ {t('invited')} ({visibleInvitations.length})
+
+
+ {sendInvitePermissions.hasAccess && (
+
- {t('members')} ({sortedMembers.length})
+ {t('requests')} ({requestsData.length})
-
-
- {t('invited')} ({visibleInvitations.length})
+ )}
+
+
+
+
+ {sortedMembers.length > 0 ? (
+ sortedMembers.map(renderMember)
+ ) : (
+
+ {t('noMembers')}
-
- {sendInvitePermissions.hasAccess && (
-
-
- {t('requests')} ({requestsData.length})
-
-
)}
-
-
-
- {/* Tab Content - Manual switching for better control */}
-
- {activeTab === 'members' && (
-
- {sortedMembers.length > 0 ? (
- sortedMembers.map(renderMember)
- ) : (
-
- {t('noMembers')}
-
- )}
-
- )}
-
- {activeTab === 'invited' && (
-
- {visibleInvitations.length > 0 ? (
- visibleInvitations.map(renderInvitation)
- ) : (
-
- {t('noInvitations')}
-
- )}
-
- )}
+
+
+
+ {visibleInvitations.length > 0 ? (
+ visibleInvitations.map(renderInvitation)
+ ) : (
+
+ {t('noInvitations')}
+
+ )}
+
- {activeTab === 'requests' && (
-
+ {sendInvitePermissions.hasAccess && (
+
{requestsData.length > 0 ? (
requestsData.map(renderRequest)
) : (
@@ -1142,81 +1113,87 @@ export const ProjectMembershipModal: React.FC = ({
{t('noPendingRequests')}
)}
-
+
)}
-
-
- {/* Invite Section */}
-
- {sendInvitePermissions.hasAccess ? (
- <>
-
- {t('inviteMembers')}
-
-
-
- setInviteAsOwner(!inviteAsOwner)}
- >
-
-
-
- setShowTooltip(!showTooltip)}
- >
-
-
-
- {showTooltip && (
-
- {t('ownerTooltip')}
-
- )}
-
+
+ {/* Invite Section */}
+
+ {sendInvitePermissions.hasAccess ? (
+ <>
+
+ {t('inviteMembers')}
+
+ {
+ if (isInviteButtonEnabled && !isSubmitting) {
+ void handleSendInvitation();
+ }
+ }}
+ returnKeyType="done"
+ keyboardType="email-address"
+ autoCapitalize="none"
+ className="mb-2"
+ />
+
+ setInviteAsOwner(!inviteAsOwner)}
>
-
- {isSubmitting ? t('sending') : t('sendInvitation')}
-
-
- >
- ) : (
-
-
-
- {t('onlyOwnersCanInvite')}
-
+
+
+
+ setShowTooltip(!showTooltip)}
+ >
+
+
- )}
-
-
- )}
-
+ {showTooltip && (
+
+ {t('ownerTooltip')}
+
+ )}
+
+ >
+ ) : (
+
+
+
+ {t('onlyOwnersCanInvite')}
+
+
+ )}
+
+
+ )}
-
-
+
+
);
};
diff --git a/components/ReportModal.tsx b/components/ReportModal.tsx
index 80b274770..6c6e2750b 100644
--- a/components/ReportModal.tsx
+++ b/components/ReportModal.tsx
@@ -1,6 +1,3 @@
-import { useAuth } from '@/contexts/AuthContext';
-import { blockService } from '@/database_services/blockService';
-import { reportService } from '@/database_services/reportService';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/ui/icon';
import { Label } from '@/components/ui/label';
@@ -8,6 +5,9 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Switch } from '@/components/ui/switch';
import { Text } from '@/components/ui/text';
import { Textarea } from '@/components/ui/textarea';
+import { useAuth } from '@/contexts/AuthContext';
+import { blockService } from '@/database_services/blockService';
+import { reportService } from '@/database_services/reportService';
import { reasonOptions } from '@/db/constants';
import { useLocalization } from '@/hooks/useLocalization';
import { useMutation, useQueryClient } from '@tanstack/react-query';
@@ -15,13 +15,15 @@ import { XIcon } from 'lucide-react-native';
import React, { useState } from 'react';
import {
Alert,
- KeyboardAvoidingView,
Modal,
Pressable,
- ScrollView,
TouchableWithoutFeedback,
View
} from 'react-native';
+import {
+ KeyboardAwareScrollView,
+ KeyboardToolbar
+} from 'react-native-keyboard-controller';
// Uncomment these imports when implementing duplicate report checking
// import { useHybridData } from '@/views/new/useHybridData';
// import { toCompilableQuery } from '@powersync/drizzle-driver';
@@ -208,100 +210,99 @@ export const ReportModal: React.FC = ({
>
-
- e.stopPropagation()}>
-
-
- {t('reportTranslation')}
-
-
-
-
+ e.stopPropagation()}>
+
+
+ {t('reportTranslation')}
+
+
+
+
-
-
-
-
- {t('selectReasonLabel')}
-
-
- handleReasonSelect(
- value as (typeof reasonOptions)[number]
- )
- }
- >
- {reasonOptions.map((option) => (
-
- ))}
-
-
+
+
+
+
+ {t('selectReasonLabel')}
+
+
+ handleReasonSelect(
+ value as (typeof reasonOptions)[number]
+ )
+ }
+ >
+ {reasonOptions.map((option) => (
+
+ ))}
+
+
+
+
+
+ {t('additionalDetails')}
+
+
+
-
-
- {t('additionalDetails')}
-
-
-
+
+
-
-
-
-
+
+
+
+
);
};
diff --git a/components/ui/drawer/index.tsx b/components/ui/drawer/index.tsx
index dd906620c..e0539046a 100644
--- a/components/ui/drawer/index.tsx
+++ b/components/ui/drawer/index.tsx
@@ -21,6 +21,18 @@ import { Pressable, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { buttonTextVariants, buttonVariants } from '../button';
import { Text, TextClassContext } from '../text';
+
+import type { BottomSheetScrollViewMethods } from '@gorhom/bottom-sheet';
+import {
+ createBottomSheetScrollableComponent,
+ SCROLLABLE_TYPE
+} from '@gorhom/bottom-sheet';
+import type { BottomSheetScrollViewProps } from '@gorhom/bottom-sheet/src/components/bottomSheetScrollable/types';
+import { memo } from 'react';
+import type { KeyboardAwareScrollViewProps } from 'react-native-keyboard-controller';
+import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
+import Reanimated from 'react-native-reanimated';
+
interface DrawerContextValue extends DrawerProps {
ref: React.RefObject | null;
open: boolean;
@@ -101,61 +113,20 @@ function Drawer({
// Memoize snapPoints array to prevent unnecessary re-renders
const memoizedSnapPoints = React.useMemo(() => {
- const serialized = JSON.stringify(stableSnapPoints);
- if (__DEV__) {
- console.log('[Drawer] Memoizing snapPoints:', serialized);
- }
return stableSnapPoints;
- }, [JSON.stringify(stableSnapPoints)]);
-
- // Track previous values for debugging
- const prevValuesRef = React.useRef({
- ref,
- isOpen,
- handleSetOpen,
- memoizedSnapPoints,
- stableEnableDynamicSizing
- });
+ }, [stableSnapPoints]);
// Memoize context value to prevent re-renders when only isOpen changes
// Only include stable props that are actually used by DrawerContent
// Don't spread drawerProps - only include what's needed
const contextValue = React.useMemo(() => {
- const newValue = {
+ return {
ref,
open: isOpen,
setOpen: handleSetOpen,
snapPoints: memoizedSnapPoints,
enableDynamicSizing: stableEnableDynamicSizing
};
-
- if (__DEV__) {
- const prev = prevValuesRef.current;
- const changes: string[] = [];
- if (prev.ref !== newValue.ref) changes.push('ref');
- if (prev.isOpen !== newValue.open)
- changes.push(`isOpen: ${prev.isOpen} -> ${newValue.open}`);
- if (prev.handleSetOpen !== newValue.setOpen)
- changes.push('handleSetOpen');
- if (prev.memoizedSnapPoints !== newValue.snapPoints)
- changes.push('snapPoints');
- if (prev.stableEnableDynamicSizing !== newValue.enableDynamicSizing)
- changes.push('enableDynamicSizing');
-
- if (changes.length > 0) {
- console.log('[Drawer] Context value changed:', changes.join(', '));
- }
-
- prevValuesRef.current = {
- ref: newValue.ref,
- isOpen: newValue.open,
- handleSetOpen: newValue.setOpen,
- memoizedSnapPoints: newValue.snapPoints,
- stableEnableDynamicSizing: newValue.enableDynamicSizing
- };
- }
-
- return newValue;
}, [
ref,
isOpen,
@@ -243,44 +214,6 @@ const DrawerContent = React.forwardRef<
>(({ className, children, ...props }, _forwardedRef) => {
const context = React.useContext(DrawerContext);
- // Track renders for debugging
- const renderCountRef = React.useRef(0);
- const prevContextRef = React.useRef(context);
-
- if (__DEV__) {
- renderCountRef.current += 1;
- const renderCount = renderCountRef.current;
-
- if (renderCount > 1 && prevContextRef.current !== context) {
- const prev = prevContextRef.current;
- const changes: string[] = [];
- if (prev?.open !== context?.open)
- changes.push(`open: ${prev?.open} -> ${context?.open}`);
- if (prev?.setOpen !== context?.setOpen) changes.push('setOpen');
- if (prev?.snapPoints !== context?.snapPoints) changes.push('snapPoints');
- if (prev?.enableDynamicSizing !== context?.enableDynamicSizing)
- changes.push('enableDynamicSizing');
- if (prev?.ref !== context?.ref) changes.push('ref');
-
- console.log(
- `[DrawerContent] Render #${renderCount} - Context changed:`,
- changes.join(', ')
- );
-
- if (renderCount > 10) {
- console.warn(
- `[DrawerContent] ⚠️ High render count: ${renderCount} renders detected!`
- );
- }
- } else if (renderCount > 1) {
- console.log(
- `[DrawerContent] Render #${renderCount} - Context unchanged (same reference)`
- );
- }
-
- prevContextRef.current = context;
- }
-
const {
open: _open,
setOpen: _setOpen,
@@ -291,15 +224,6 @@ const DrawerContent = React.forwardRef<
// Extract setOpen to avoid depending on entire context object (which changes on every render)
const setOpen = context?.setOpen;
- const prevSetOpenRef = React.useRef(setOpen);
- if (__DEV__ && prevSetOpenRef.current !== setOpen) {
- console.log(
- '[DrawerContent] setOpen changed:',
- prevSetOpenRef.current !== setOpen
- );
- prevSetOpenRef.current = setOpen;
- }
-
const handleSheetChanges = React.useCallback(
(index: number) => {
if (index === -1) {
@@ -351,12 +275,14 @@ const DrawerContent = React.forwardRef<
>
{/* Re-provide DrawerContext inside the portal so children can access it */}
-
{children}
-
+
);
@@ -420,6 +346,18 @@ function DrawerDescription({
);
}
+const AnimatedScrollView =
+ Reanimated.createAnimatedComponent(
+ KeyboardAwareScrollView
+ );
+const DrawerScrollViewComponent = createBottomSheetScrollableComponent<
+ BottomSheetScrollViewMethods,
+ BottomSheetScrollViewProps
+>(SCROLLABLE_TYPE.SCROLLVIEW, AnimatedScrollView);
+const DrawerKeyboardAwareScrollView = memo(DrawerScrollViewComponent);
+
+DrawerKeyboardAwareScrollView.displayName = 'DrawerKeyboardAwareScrollView';
+
export {
BottomSheetModal,
BottomSheetModalProvider,
@@ -430,6 +368,7 @@ export {
DrawerFooter,
DrawerHeader,
DrawerInput,
+ DrawerKeyboardAwareScrollView,
DrawerScrollView,
DrawerTitle,
DrawerTrigger
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
index 6d3a4bde7..29067a040 100644
--- a/components/ui/input.tsx
+++ b/components/ui/input.tsx
@@ -6,6 +6,7 @@ import { EyeIcon, EyeOffIcon } from 'lucide-react-native';
import * as React from 'react';
import type { TextInputProps } from 'react-native';
import { Platform, Pressable, TextInput, View } from 'react-native';
+import { KeyboardController } from 'react-native-keyboard-controller';
import { DrawerInput } from './drawer';
import { Icon } from './icon';
@@ -119,6 +120,7 @@ interface InputProps
drawerInput?: boolean;
hideEye?: boolean;
mask?: boolean;
+ type?: 'next';
}
const Input = React.forwardRef<
@@ -134,6 +136,8 @@ const Input = React.forwardRef<
prefixStyling = false,
suffixStyling = false,
drawerInput = false,
+ type,
+ onSubmitEditing,
hideEye,
secureTextEntry,
mask,
@@ -185,6 +189,14 @@ const Input = React.forwardRef<
)}
KeyboardController.setFocusTo('next')
+ : undefined)
+ }
+ submitBehavior={type === 'next' ? 'submit' : undefined}
// @ts-expect-error - ref is not passed the same type as TextInput
ref={ref}
className={cn(
diff --git a/package-lock.json b/package-lock.json
index ff94bd3a1..8c2c16869 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -85,6 +85,7 @@
"react-native": "0.79.5",
"react-native-element-dropdown": "2.12.2",
"react-native-gesture-handler": "~2.24.0",
+ "react-native-keyboard-controller": "^1.19.5",
"react-native-onboarding": "^1.0.6",
"react-native-pager-view": "^7.0.0",
"react-native-reanimated": "~4.1.0",
@@ -131,6 +132,7 @@
"jiti": "^2.6.1",
"knip": "^5.64.2",
"metro-react-native-babel-transformer": "^0.77.0",
+ "patch-package": "^8.0.1",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"react-native-svg-transformer": "^1.5.1",
@@ -8758,6 +8760,13 @@
"license": "Apache-2.0",
"peer": true
},
+ "node_modules/@yarnpkg/lockfile": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -17590,6 +17599,26 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"license": "MIT"
},
+ "node_modules/json-stable-stringify": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
+ "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "isarray": "^2.0.5",
+ "jsonify": "^0.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -17618,6 +17647,16 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/jsonify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
+ "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
+ "dev": true,
+ "license": "Public Domain",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -17651,6 +17690,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/klaw-sync": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
+ "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.11"
+ }
+ },
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -19999,6 +20048,127 @@
"node": ">= 0.8"
}
},
+ "node_modules/patch-package": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
+ "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@yarnpkg/lockfile": "^1.1.0",
+ "chalk": "^4.1.2",
+ "ci-info": "^3.7.0",
+ "cross-spawn": "^7.0.3",
+ "find-yarn-workspace-root": "^2.0.0",
+ "fs-extra": "^10.0.0",
+ "json-stable-stringify": "^1.0.2",
+ "klaw-sync": "^6.0.0",
+ "minimist": "^1.2.6",
+ "open": "^7.4.2",
+ "semver": "^7.5.3",
+ "slash": "^2.0.0",
+ "tmp": "^0.2.4",
+ "yaml": "^2.2.2"
+ },
+ "bin": {
+ "patch-package": "index.js"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">5"
+ }
+ },
+ "node_modules/patch-package/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/patch-package/node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/patch-package/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/patch-package/node_modules/open": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
+ "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0",
+ "is-wsl": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/patch-package/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/patch-package/node_modules/slash": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/patch-package/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/path-dirname": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
@@ -21167,6 +21337,20 @@
"react-native": "*"
}
},
+ "node_modules/react-native-keyboard-controller": {
+ "version": "1.19.5",
+ "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.19.5.tgz",
+ "integrity": "sha512-OvtEfgt8EzEDz2J045CQ1aKMO0nWzsEthHWhYiYMv5QLMT9LY50GbHls4sIg8oobrHX3qi5zVgAUG1jCs0iIWg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-native-is-edge-to-edge": "^1.2.1"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*",
+ "react-native-reanimated": ">=3.0.0"
+ }
+ },
"node_modules/react-native-onboarding": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/react-native-onboarding/-/react-native-onboarding-1.0.6.tgz",
@@ -23753,6 +23937,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/tmp": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
diff --git a/package.json b/package.json
index 23b3eeab3..3fe454a76 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "langquest",
"main": "expo-router/entry",
- "version": "2.0.1",
+ "version": "2.0.3",
"scripts": {
"start": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev",
@@ -43,7 +43,8 @@
"supabase:functions:deploy:send-email:dev": "npm run supabase functions deploy send-email -- --no-verify-jwt --project-ref yjgdgsycxmlvaiuynlbv",
"supabase:functions:deploy:send-email:prod": "npm run supabase functions deploy send-email -- --no-verify-jwt --project-ref unsxkmlcyxgtgmtzfonb",
"shadcn": "shadcn",
- "knip": "knip"
+ "knip": "knip",
+ "postinstall": "patch-package"
},
"jest": {
"preset": "jest-expo"
@@ -126,6 +127,7 @@
"react-native": "0.79.5",
"react-native-element-dropdown": "2.12.2",
"react-native-gesture-handler": "~2.24.0",
+ "react-native-keyboard-controller": "^1.19.5",
"react-native-onboarding": "^1.0.6",
"react-native-pager-view": "^7.0.0",
"react-native-reanimated": "~4.1.0",
@@ -172,6 +174,7 @@
"jiti": "^2.6.1",
"knip": "^5.64.2",
"metro-react-native-babel-transformer": "^0.77.0",
+ "patch-package": "^8.0.1",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"react-native-svg-transformer": "^1.5.1",
diff --git a/patches/@gorhom+bottom-sheet+5.2.6.patch b/patches/@gorhom+bottom-sheet+5.2.6.patch
new file mode 100644
index 000000000..e0519d2ae
--- /dev/null
+++ b/patches/@gorhom+bottom-sheet+5.2.6.patch
@@ -0,0 +1,16 @@
+# Fix for react-native-reanimated 4 compatibility
+# Based on PR: https://github.com/gorhom/react-native-bottom-sheet/pull/2518
+# Removes Animated.EasingFunction type annotation which doesn't exist in reanimated 4
+diff --git a/node_modules/@gorhom/bottom-sheet/src/constants.ts b/node_modules/@gorhom/bottom-sheet/src/constants.ts
+index 4159132..838d9c9 100644
+--- a/node_modules/@gorhom/bottom-sheet/src/constants.ts
++++ b/node_modules/@gorhom/bottom-sheet/src/constants.ts
+@@ -69,7 +69,7 @@ enum SNAP_POINT_TYPE {
+ DYNAMIC = 1,
+ }
+
+-const ANIMATION_EASING: Animated.EasingFunction = Easing.out(Easing.exp);
++const ANIMATION_EASING = Easing.out(Easing.exp);
+ const ANIMATION_DURATION = 250;
+
+ const ANIMATION_CONFIGS = Platform.select({
diff --git a/views/ForgotPasswordView.tsx b/views/ForgotPasswordView.tsx
index 04a7e4f17..f5f996a1b 100644
--- a/views/ForgotPasswordView.tsx
+++ b/views/ForgotPasswordView.tsx
@@ -19,9 +19,10 @@ import { safeNavigate } from '@/utils/sharedUtils';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { LockIcon, MailIcon, WifiOffIcon } from 'lucide-react-native';
-import React, { useRef } from 'react';
+import React from 'react';
import { useForm } from 'react-hook-form';
-import { Alert, ScrollView, View } from 'react-native';
+import { Alert, View } from 'react-native';
+import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
import { z } from 'zod';
const { supabaseConnector } = system;
@@ -35,8 +36,6 @@ export default function ForgotPasswordView({
}) {
const { t } = useLocalization();
const isOnline = useNetworkStatus();
- const scrollViewRef = useRef(null);
-
const formSchema = z.object({
email: z
.email(t('enterValidEmail'))
@@ -81,90 +80,73 @@ export default function ForgotPasswordView({
}
});
- // Auto-scroll to input when focused
- const handleInputFocus = (offset: number) => {
- setTimeout(() => {
- scrollViewRef.current?.scrollTo({
- y: offset,
- animated: true
- });
- }, 100);
- };
-
return (
);
}
diff --git a/views/ProfileView.tsx b/views/ProfileView.tsx
index f021a0a86..26d6bc0e9 100644
--- a/views/ProfileView.tsx
+++ b/views/ProfileView.tsx
@@ -8,6 +8,7 @@ import {
FormField,
FormItem,
FormMessage,
+ FormSubmit,
transformInputProps
} from '@/components/ui/form';
import { Icon } from '@/components/ui/icon';
@@ -45,7 +46,8 @@ import {
} from 'lucide-react-native';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
-import { Platform, Alert as RNAlert, ScrollView, View } from 'react-native';
+import { Platform, Alert as RNAlert, View } from 'react-native';
+import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
import { z } from 'zod';
// Validation schema
@@ -129,7 +131,7 @@ export default function ProfileView() {
}
}, [currentUser, form]);
- const { mutateAsync: updateProfile, isPending } = useMutation({
+ const { mutateAsync: updateProfile } = useMutation({
mutationFn: async (data: z.infer) => {
if (!currentUser) return;
@@ -188,193 +190,174 @@ export default function ProfileView() {
}
});
+ const handleFormSubmit = form.handleSubmit((data) => updateProfile(data));
+
return (
);
}
diff --git a/views/RegisterView.tsx b/views/RegisterView.tsx
index f87ff42ff..23ce5cae7 100644
--- a/views/RegisterView.tsx
+++ b/views/RegisterView.tsx
@@ -1,4 +1,5 @@
import { LanguageSelect } from '@/components/language-select';
+import { Alert, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
@@ -21,9 +22,10 @@ import { cn } from '@/utils/styleUtils';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { LockIcon, MailIcon, UserIcon, WifiOffIcon } from 'lucide-react-native';
-import React, { useEffect, useRef } from 'react';
+import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
-import { Alert, Pressable, ScrollView, View } from 'react-native';
+import { Pressable, Alert as RNAlert, View } from 'react-native';
+import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
import { z } from 'zod';
const { supabaseConnector } = system;
@@ -39,8 +41,6 @@ export default function RegisterView({
const isOnline = useNetworkStatus();
const currentLanguage = useLocalStore((state) => state.uiLanguage);
const dateTermsAccepted = useLocalStore((state) => state.dateTermsAccepted);
- const scrollViewRef = useRef(null);
-
const formSchema = z
.object({
email: z
@@ -100,7 +100,7 @@ export default function RegisterView({
);
},
onError: (error) => {
- Alert.alert(
+ RNAlert.alert(
t('error') || 'Error',
error instanceof Error
? error.message
@@ -129,31 +129,23 @@ export default function RegisterView({
}
}, [form, sharedAuthInfo?.email]);
- // Auto-scroll to input when focused
- const handleInputFocus = (offset: number) => {
- setTimeout(() => {
- scrollViewRef.current?.scrollTo({
- y: offset,
- animated: true
- });
- }, 100);
- };
+ const handleFormSubmit = form.handleSubmit((data) => register(data));
return (
-
+
+
+ >
);
}
diff --git a/views/SignInView.tsx b/views/SignInView.tsx
index 1074b97db..8377a45fd 100644
--- a/views/SignInView.tsx
+++ b/views/SignInView.tsx
@@ -20,9 +20,10 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { useRouter } from 'expo-router';
import { LockIcon, MailIcon, WifiOffIcon } from 'lucide-react-native';
-import React, { useRef } from 'react';
+import React from 'react';
import { useForm } from 'react-hook-form';
-import { Alert, Pressable, ScrollView, View } from 'react-native';
+import { Alert, Pressable, View } from 'react-native';
+import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
import { z } from 'zod';
const { supabaseConnector } = system;
@@ -40,8 +41,6 @@ export default function SignInView({
const { t } = useLocalization();
const router = useRouter();
const isOnline = useNetworkStatus();
- const scrollViewRef = useRef(null);
-
const formSchema = z.object({
email: z
.email(t('enterValidEmail'))
@@ -84,31 +83,22 @@ export default function SignInView({
const form = useForm>({
resolver: zodResolver(formSchema),
- disabled: isPending,
defaultValues: {
email: sharedAuthInfo?.email
}
});
- // Auto-scroll to input when focused
- const handleInputFocus = (offset: number) => {
- setTimeout(() => {
- scrollViewRef.current?.scrollTo({
- y: offset,
- animated: true
- });
- }, 100);
- };
+ const handleFormSubmit = form.handleSubmit((data) => login(data));
return (
);
diff --git a/views/new/NextGenAssetsView.tsx b/views/new/NextGenAssetsView.tsx
index dd206529c..b1b9ffd03 100644
--- a/views/new/NextGenAssetsView.tsx
+++ b/views/new/NextGenAssetsView.tsx
@@ -668,6 +668,7 @@ export default function NextGenAssetsView() {
prefix={SearchIcon}
prefixStyling={false}
size="sm"
+ returnKeyType="search"
suffix={
isFetching && searchQuery ? (
diff --git a/views/new/NextGenProjectsView.tsx b/views/new/NextGenProjectsView.tsx
index a1fb2c7b8..7749a0f8d 100644
--- a/views/new/NextGenProjectsView.tsx
+++ b/views/new/NextGenProjectsView.tsx
@@ -813,6 +813,7 @@ export default function NextGenProjectsView() {
prefix={SearchIcon}
prefixStyling={false}
size="sm"
+ returnKeyType="search"
suffix={
isFetchingProjects && searchQuery ? (
& {
images: string[];
content: (typeof asset_content_link.$inferSelect)[];
- votes: Array<{
+ votes: {
id: string;
asset_id: string;
creator_id: string;
polarity: 'up' | 'down';
active: boolean;
created_at: string;
- }>;
+ }[];
}
>({
dataType: 'translation',
@@ -127,14 +129,14 @@ function useNextGenTranslation(assetId: string) {
(Omit & {
images: string;
content?: (typeof asset_content_link.$inferSelect)[];
- votes?: Array<{
+ votes?: {
id: string;
asset_id: string;
creator_id: string;
polarity: 'up' | 'down';
active: boolean;
created_at: string;
- }>;
+ }[];
})[]
>();
@@ -414,297 +416,293 @@ export default function NextGenTranslationModal({
animationType="slide"
onRequestClose={handleClose}
>
-
-
-
- e.stopPropagation()}>
-
- {/* Header */}
-
- {t('translation')}
-
- {/* Edit/Transcription button */}
- {allowEditing && (
- (
-
- )}
+
+
+ e.stopPropagation()}>
+
+ {/* Header */}
+
+ {t('translation')}
+
+ {/* Edit/Transcription button */}
+ {allowEditing && (
+ (
+
+ )}
+ />
+ )}
+ {isOwnTranslation && allowSettings && (
+
+ )}
+ {!isOwnTranslation && (
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : asset ? (
+
+ {/* Translation Text */}
+ {isEditing ? (
+
-
-
- {isLoading ? (
-
-
-
- ) : asset ? (
-
- {/* Translation Text */}
- {isEditing ? (
-
+ {/* Voting Section with PrivateAccessGate */}
+ {!isOwnTranslation &&
+ !isEditing &&
+ !hasReported &&
+ // Show login prompt for anonymous users
+ (!isAuthenticated ? (
+
+
+ {t('pleaseLogInToVoteOnTranslations')}
+
+ {
+ onOpenChange(false);
+ setAuthView('sign-in');
+ }}
+ className="mt-4"
+ >
+ {t('signIn') || 'Sign In'}
+
+
) : (
-
- {assetText || '(No text)'}
-
- )}
-
- {/* Audio Player */}
- {audioSegments.length > 0 && !isEditing && (
-
-
-
- )}
-
- {/* Submit button for transcription */}
- {isEditing && (
-
- {t('submitTranscription')}
-
- )}
-
- {/* Voting Section with PrivateAccessGate */}
- {!isOwnTranslation &&
- !isEditing &&
- !hasReported &&
- // Show login prompt for anonymous users
- (!isAuthenticated ? (
-
-
- {t('pleaseLogInToVoteOnTranslations')}
-
- {
- onOpenChange(false);
- setAuthView('sign-in');
- }}
- className="mt-4"
- >
- {t('signIn') || 'Sign In'}
-
-
- ) : (
-
-
-
-
- {t('voting')}
-
-
-
- handleVote({ voteType: 'up' })}
- disabled={isVotePending}
- className="flex-row items-center justify-center bg-green-500 px-6 py-3"
- >
- {pendingVoteType === 'up' ? (
-
- ) : (
-
- )}
-
-
-
-
-
- Net: {upVotes - downVotes > 0 ? '+' : ''}
- {upVotes - downVotes}
-
-
-
-
- {upVotes}
-
-
- {downVotes}
-
-
+
+
+
+ {t('voting')}
+
+
+
+ handleVote({ voteType: 'up' })}
+ disabled={isVotePending}
+ className="flex-row items-center justify-center bg-green-500 px-6 py-3"
+ >
+ {pendingVoteType === 'up' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Net: {upVotes - downVotes > 0 ? '+' : ''}
+ {upVotes - downVotes}
+
+
+
+
+ {upVotes}
+
+
+ {downVotes}
+
-
- handleVote({ voteType: 'down' })
- }
- disabled={isVotePending}
- className="flex-row items-center justify-center bg-red-600 px-6 py-3"
- >
- {pendingVoteType === 'down' ? (
-
- ) : (
-
- )}
-
+ handleVote({ voteType: 'down' })}
+ disabled={isVotePending}
+ className="flex-row items-center justify-center bg-red-600 px-6 py-3"
+ >
+ {pendingVoteType === 'down' ? (
+
+ ) : (
+
+ )}
+
-
- ))}
- {isOwnTranslation ? (
- setShowSettingsModal(false)}
- translationId={assetId}
- />
- ) : (
- setShowReportModal(false)}
- recordId={assetId}
- recordTable="asset"
- creatorId={asset.creator_id ?? undefined}
- hasAlreadyReported={hasReported}
- onReportSubmitted={(contentBlocked) => {
- void refetch();
- // Close the translation modal if content was blocked
- if (contentBlocked) {
- void handleClose();
- }
- }}
- />
- )}
- {/* Debug Info */}
- {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
- {SHOW_DEV_ELEMENTS && (
-
-
- {isOnline ? '🟢 Online' : '🔴 Offline'} • ID:{' '}
- {asset.id.substring(0, 8)}...
-
-
- )}
-
- ) : (
-
-
- {t('translationNotFound')}
-
-
- )}
-
-
-
-
-
-
+
+
+ ))}
+ {isOwnTranslation ? (
+ setShowSettingsModal(false)}
+ translationId={assetId}
+ />
+ ) : (
+ setShowReportModal(false)}
+ recordId={assetId}
+ recordTable="asset"
+ creatorId={asset.creator_id ?? undefined}
+ hasAlreadyReported={hasReported}
+ onReportSubmitted={(contentBlocked) => {
+ void refetch();
+ // Close the translation modal if content was blocked
+ if (contentBlocked) {
+ void handleClose();
+ }
+ }}
+ />
+ )}
+ {/* Debug Info */}
+ {}
+ {SHOW_DEV_ELEMENTS && (
+
+
+ {isOnline ? '🟢 Online' : '🔴 Offline'} • ID:{' '}
+ {asset.id.substring(0, 8)}...
+
+
+ )}
+
+ ) : (
+
+
+ {t('translationNotFound')}
+
+
+ )}
+
+
+
+
+
+
);
}
diff --git a/views/new/OnboardingFlow.tsx b/views/new/OnboardingFlow.tsx
index 75abf2223..9767bd5bb 100644
--- a/views/new/OnboardingFlow.tsx
+++ b/views/new/OnboardingFlow.tsx
@@ -32,13 +32,15 @@ import React, { useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import {
ActivityIndicator,
- KeyboardAvoidingView,
Modal,
- Platform,
Pressable,
ScrollView,
View
} from 'react-native';
+import {
+ KeyboardAwareScrollView,
+ KeyboardToolbar
+} from 'react-native-keyboard-controller';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import uuid from 'react-native-uuid';
import { z } from 'zod';
@@ -290,6 +292,8 @@ export function OnboardingFlow({ visible, onClose }: OnboardingFlowProps) {
}
};
+ const handleFormSubmit = languageForm.handleSubmit(handleCreateLanguage);
+
const handleProjectSelect = (project: (typeof projectsByLanguage)[0]) => {
// Navigate to existing project
goToProject({
@@ -365,11 +369,7 @@ export function OnboardingFlow({ visible, onClose }: OnboardingFlowProps) {
animationType="slide"
onRequestClose={handleClose}
>
-
+
{/* Progress Indicator */}
{step !== 'create-language' && (
@@ -390,7 +390,12 @@ export function OnboardingFlow({ visible, onClose }: OnboardingFlowProps) {
{/* Content */}
-
+
{/* Step 1: Region Selection */}
{step === 'region' && (
@@ -702,6 +707,7 @@ export function OnboardingFlow({ visible, onClose }: OnboardingFlowProps) {
onChangeText={(text) =>
languageForm.setValue('native_name', text)
}
+ type="next"
editable={!isLoading}
/>
@@ -711,6 +717,7 @@ export function OnboardingFlow({ visible, onClose }: OnboardingFlowProps) {
onChangeText={(text) =>
languageForm.setValue('english_name', text)
}
+ type="next"
editable={!isLoading}
/>
@@ -720,6 +727,7 @@ export function OnboardingFlow({ visible, onClose }: OnboardingFlowProps) {
onChangeText={(text) =>
languageForm.setValue('iso639_3', text)
}
+ type="next"
editable={!isLoading}
/>
@@ -727,11 +735,17 @@ export function OnboardingFlow({ visible, onClose }: OnboardingFlowProps) {
placeholder={t('locale') + ' (Optional)'}
value={languageForm.watch('locale')}
onChangeText={(text) => languageForm.setValue('locale', text)}
+ onSubmitEditing={() => {
+ if (!isLoading && languageForm.formState.isValid) {
+ void handleFormSubmit();
+ }
+ }}
+ returnKeyType="done"
editable={!isLoading}
/>
@@ -747,7 +761,7 @@ export function OnboardingFlow({ visible, onClose }: OnboardingFlowProps) {
)}
-
+
{/* Footer with Back button */}
{step !== 'region' && (
@@ -757,7 +771,8 @@ export function OnboardingFlow({ visible, onClose }: OnboardingFlowProps) {
)}
-
+
+
);
}
diff --git a/views/new/ProjectDirectoryView.tsx b/views/new/ProjectDirectoryView.tsx
index 7a5bf0a0d..ae950b25f 100644
--- a/views/new/ProjectDirectoryView.tsx
+++ b/views/new/ProjectDirectoryView.tsx
@@ -1015,6 +1015,7 @@ export default function ProjectDirectoryView() {
prefix={SearchIcon}
prefixStyling={false}
size="sm"
+ returnKeyType="search"
suffix={
questListFetching && searchQuery ? (
diff --git a/views/new/SimpleOnboardingFlow.tsx b/views/new/SimpleOnboardingFlow.tsx
index 6a0fe07d4..b411a322e 100644
--- a/views/new/SimpleOnboardingFlow.tsx
+++ b/views/new/SimpleOnboardingFlow.tsx
@@ -16,14 +16,11 @@ import {
XIcon
} from 'lucide-react-native';
import React, { useState } from 'react';
+import { Modal, Pressable, View } from 'react-native';
import {
- KeyboardAvoidingView,
- Modal,
- Platform,
- Pressable,
- ScrollView,
- View
-} from 'react-native';
+ KeyboardAwareScrollView,
+ KeyboardToolbar
+} from 'react-native-keyboard-controller';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { AnimatedOnboardingIcon } from './onboarding/AnimatedOnboardingIcon';
import { AnimatedStepContent } from './onboarding/AnimatedStepContent';
@@ -176,11 +173,7 @@ export function SimpleOnboardingFlow({
animationType="slide"
onRequestClose={handleClose}
>
-
+
{/* PortalHost for Select dropdowns inside Modal */}
@@ -196,7 +189,12 @@ export function SimpleOnboardingFlow({
{/* Content */}
-
+
{/* Step 0: Vision Screen */}
{step === 'vision' && (
@@ -530,7 +528,7 @@ export function SimpleOnboardingFlow({
)}
-
+
{/* Footer with Back button */}
{step !== 'vision' && step !== 'create-project-simple' && (
@@ -540,7 +538,8 @@ export function SimpleOnboardingFlow({
)}
-
+
+
);
}
diff --git a/views/new/recording/components/RenameAssetModal.tsx b/views/new/recording/components/RenameAssetModal.tsx
index ef7140da7..7200477dc 100644
--- a/views/new/recording/components/RenameAssetModal.tsx
+++ b/views/new/recording/components/RenameAssetModal.tsx
@@ -6,15 +6,14 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Text } from '@/components/ui/text';
import React from 'react';
-import {
- Animated,
- KeyboardAvoidingView,
- Modal,
- Platform,
- Pressable,
- View
-} from 'react-native';
import type { TextInput } from 'react-native';
+import { Modal, Pressable, View } from 'react-native';
+import Animated, {
+ Easing,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming
+} from 'react-native-reanimated';
interface RenameAssetModalProps {
isVisible: boolean;
@@ -30,52 +29,66 @@ export function RenameAssetModal({
onSave
}: RenameAssetModalProps) {
const [name, setName] = React.useState(currentName);
+ const [modalVisible, setModalVisible] = React.useState(false);
const inputRef = React.useRef(null);
- const fadeAnim = React.useRef(new Animated.Value(0)).current;
- const scaleAnim = React.useRef(new Animated.Value(0.9)).current;
+ const opacity = useSharedValue(0);
+ const scale = useSharedValue(0.9);
// Update local state when currentName changes
React.useEffect(() => {
setName(currentName);
}, [currentName]);
- // Animate in/out and focus
+ // Handle modal visibility with exit animation
React.useEffect(() => {
if (isVisible) {
- // Animate in
- Animated.parallel([
- Animated.timing(fadeAnim, {
- toValue: 1,
- duration: 200,
- useNativeDriver: true
- }),
- Animated.spring(scaleAnim, {
- toValue: 1,
- friction: 8,
- tension: 65,
- useNativeDriver: true
- })
- ]).start(() => {
- // Focus after animation completes
- setTimeout(() => {
- inputRef.current?.focus();
- }, 50);
+ // Show modal immediately
+ setModalVisible(true);
+ // Quick, snappy animation (Emil Kowalski style)
+ opacity.value = withTiming(1, {
+ duration: 150,
+ easing: Easing.out(Easing.ease)
});
+ scale.value = withTiming(1, {
+ duration: 150,
+ easing: Easing.out(Easing.ease)
+ });
+
+ // Focus after animation completes
+ const focusTimer = setTimeout(() => {
+ inputRef.current?.focus();
+ }, 150);
- // Backup focus attempt in case animation callback doesn't fire
+ // Backup focus attempt
const backupTimer = setTimeout(() => {
inputRef.current?.focus();
- }, 300);
+ }, 200);
return () => {
+ clearTimeout(focusTimer);
clearTimeout(backupTimer);
};
} else {
- // Reset animations
- fadeAnim.setValue(0);
- scaleAnim.setValue(0.9);
+ // Exit animation - quick fade out (ease-out for responsiveness)
+ opacity.value = withTiming(0, {
+ duration: 100,
+ easing: Easing.out(Easing.ease)
+ });
+ scale.value = withTiming(0.9, {
+ duration: 100,
+ easing: Easing.out(Easing.ease)
+ });
+
+ // Hide modal after exit animation completes
+ const hideTimer = setTimeout(() => {
+ setModalVisible(false);
+ }, 100);
+
+ return () => {
+ clearTimeout(hideTimer);
+ };
}
- }, [isVisible, fadeAnim, scaleAnim]);
+ }, [isVisible, opacity, scale]);
const handleSave = () => {
const trimmedName = name.trim();
@@ -90,63 +103,59 @@ export function RenameAssetModal({
onClose();
};
- if (!isVisible) return null;
+ const animatedStyle = useAnimatedStyle(() => ({
+ opacity: opacity.value,
+ transform: [{ scale: scale.value }]
+ }));
+
+ if (!modalVisible) return null;
return (
-
-
-
- e.stopPropagation()}>
-
-
- Rename Asset
-
-
-
-
-
-
- Cancel
-
-
- Save
-
-
+
+ e.stopPropagation()}>
+
+
+ Rename Asset
+
+
+
+
+
+
+ Cancel
+
+
+ Save
+
-
-
-
-
+
+
+
+
);
}