Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import { Tabs } from 'expo-router';
import React from 'react';
import React, { useMemo } from 'react';

import { HapticTab } from '@/components/haptic-tab';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { ErrorBoundary } from '@/src/components';

export default function TabLayout() {
const TabLayout = () => {
const colorScheme = useColorScheme();

const screenOptions = useMemo(
() => ({
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarButton: HapticTab,
}),
[colorScheme]
);

return (
<ErrorBoundary boundaryName="TabsLayout">
<Tabs
// Keep all tab screens mounted so state and scroll positions survive tab switches
detachInactiveScreens={false}
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarButton: HapticTab,
}}
screenOptions={screenOptions}
>
<Tabs.Screen
name="index"
Expand Down Expand Up @@ -47,4 +52,6 @@ export default function TabLayout() {
</Tabs>
</ErrorBoundary>
);
}
};

export default TabLayout;
4 changes: 2 additions & 2 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { AnalyticsProvider, ErrorBoundary, OfflineIndicatorProvider } from '../s
import { useAnalytics } from '../src/hooks';
import { useDeepLink } from '../src/hooks/useDeepLink';
import { sessionRestorationService } from '../src/services/sessionRestoration';
import { useAppStore } from '../src/store';
import { useTheme } from '../src/store/selectors';
import { getPathFromDeepLink } from '../src/utils/linkParser';
import { prefetchExternalResources } from '../src/utils/resourceHints';

Expand Down Expand Up @@ -40,7 +40,7 @@ const ScreenTracker = () => {

// Sync global theme to NativeWind colorScheme
const ThemeSync = () => {
const { theme } = useAppStore();
const theme = useTheme();
const { setColorScheme } = useColorScheme();

useEffect(() => {
Expand Down
48 changes: 20 additions & 28 deletions src/components/common/AppText.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Text as RNText, TextProps, StyleSheet } from 'react-native';

import { useDynamicFontSize } from '../../hooks';

interface AppTextProps extends TextProps {
Expand All @@ -10,34 +11,25 @@ interface AppTextProps extends TextProps {
fixed?: boolean;
}

/**
* A wrapper around React Native's Text component that uses the useDynamicFontSize hook
* to ensure consistent scaling across the application.
*/
export const AppText: React.FC<AppTextProps> = ({ style, fixed = false, ...props }) => {
const { scale } = useDynamicFontSize();

// We flatten the style to easily extract and modify the fontSize
const flattenedStyle = StyleSheet.flatten(style) || {};
const AppTextInner: React.FC<AppTextProps> = ({ style, fixed = false, ...props }) => {
const { fontScale } = useDynamicFontSize();

const dynamicStyle = { ...flattenedStyle };
const dynamicStyle = useMemo(() => {
const flat = StyleSheet.flatten(style) || {};
if (fixed || !flat.fontSize) return flat;

if (!fixed && flattenedStyle.fontSize) {
dynamicStyle.fontSize = scale(flattenedStyle.fontSize);
return {
...flat,
fontSize: flat.fontSize * fontScale,
...(flat.lineHeight ? { lineHeight: flat.lineHeight * fontScale } : {}),
};
}, [style, fixed, fontScale]);

// Also scale lineHeight if it exists to maintain proportions
if (flattenedStyle.lineHeight) {
dynamicStyle.lineHeight = scale(flattenedStyle.lineHeight);
}
}

return (
<RNText
{...props}
style={dynamicStyle}
// We set allowFontScaling to false because we are manually scaling
// via the dynamicStyle to have explicit control via our hook.
allowFontScaling={false}
/>
);
return <RNText {...props} style={dynamicStyle} allowFontScaling={false} />;
};

/**
* A wrapper around React Native's Text component that uses the useDynamicFontSize hook
* to ensure consistent scaling across the application.
*/
export const AppText = React.memo(AppTextInner);
9 changes: 5 additions & 4 deletions src/components/mobile/LessonCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';

import { useDebounceCallback } from '../../hooks';
import { useAnalytics } from '../../hooks/useAnalytics';
import { Lesson, CourseProgress } from '../../types/course';
import { AnalyticsEvent } from '../../utils/trackingEvents';
import { useDebounceCallback } from '../../hooks';

const { width: SCREEN_WIDTH } = Dimensions.get('window');

Expand Down Expand Up @@ -48,7 +48,7 @@ const LessonCarousel = ({
renderLessonContent,
onLastLessonNext,
isLastLessonInSection = false,
}: LessonCarouselProps) {
}: LessonCarouselProps) => {
const { trackEvent } = useAnalytics();
const scrollViewRef = useRef<ScrollView>(null);
const [currentIndex, setCurrentIndex] = useState(0);
Expand Down Expand Up @@ -95,7 +95,7 @@ const LessonCarousel = ({
const debouncedScroll = useDebounceCallback((offsetX: number) => {
const index = Math.round(offsetX / SCREEN_WIDTH);
if (index >= 0 && index < lessons.length) {
setCurrentIndex((prevIndex) => {
setCurrentIndex(prevIndex => {
if (index !== prevIndex) {
const lesson = lessons[index];
onLessonChange(lesson.id, index);
Expand Down Expand Up @@ -235,6 +235,7 @@ const LessonCarousel = ({
pagingEnabled
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={handleMomentumScrollEnd}
onScroll={handleScroll}
scrollEventThrottle={16}
decelerationRate="fast"
snapToInterval={SCREEN_WIDTH}
Expand Down
45 changes: 25 additions & 20 deletions src/components/mobile/MobileCourseViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import { AppText as Text } from '../common/AppText';
import { SafeAreaView } from 'react-native-safe-area-context';

import BookmarkButton from './BookmarkButton';
import { CourseViewerSkeleton } from './CourseViewerSkeleton';
import LessonCarousel from './LessonCarousel';
import MobileSyllabus from './MobileSyllabus';
import { useCourseProgress, useDynamicFontSize } from '../../hooks';
import { SafeAreaView } from "react-native-safe-area-context";
import logger from "../../utils/logger";
import PrimaryButton from "../common/PrimaryButton";
import BookmarkButton from "./BookmarkButton";
import LessonCarousel from "./LessonCarousel";
import MobileSyllabus from "./MobileSyllabus";
import { useAnalytics } from "../../hooks/useAnalytics";
import { Course, Lesson, Note } from "../../types/course";
import { AnalyticsEvent, ScreenName } from "../../utils/trackingEvents";
import { ErrorBoundary } from "../common/ErrorBoundary";
import { CourseViewerSkeleton } from "./CourseViewerSkeleton";
import { useAnalytics } from '../../hooks/useAnalytics';
import { Course, Lesson, Note } from '../../types/course';
import { logger } from '../../utils/logger';
import { AnalyticsEvent, ScreenName } from '../../utils/trackingEvents';
import { AppText as Text } from '../common/AppText';
import { ErrorBoundary } from '../common/ErrorBoundary';
import PrimaryButton from '../common/PrimaryButton';

/**
* Props for the MobileCourseViewer component
Expand All @@ -41,13 +42,13 @@ interface MobileCourseViewerProps {

type ViewMode = 'lesson' | 'syllabus' | 'notes';

export default function MobileCourseViewer({
const MobileCourseViewer: React.FC<MobileCourseViewerProps> = ({
course,
initialLessonId,
initialViewMode,
onBack,
navigation,
}: MobileCourseViewerProps) {
}: MobileCourseViewerProps) => {
const { scale } = useDynamicFontSize();
const { trackEvent, trackScreen } = useAnalytics();
const [viewMode, setViewMode] = useState<ViewMode>(initialViewMode || 'lesson');
Expand All @@ -64,7 +65,6 @@ export default function MobileCourseViewer({
const {
progress,
isLoading,
updateLessonProgress,
markLessonComplete,
setCurrentLesson,
addBookmark,
Expand All @@ -80,8 +80,11 @@ export default function MobileCourseViewer({
autoSync: true,
});

// Get all lessons in order
const allLessons = course.sections.flatMap(section => section.lessons.map(lesson => lesson));
// Memoized — recalculates only when the sections array reference changes (i.e. new course data).
const allLessons = useMemo(
() => course.sections.flatMap(section => section.lessons),
[course.sections]
);

// Helper to get section ID for a lesson
const getSectionIdForLesson = useCallback(
Expand Down Expand Up @@ -142,7 +145,7 @@ export default function MobileCourseViewer({
} catch (error) {
logger.error('Error in MobileCourseViewer:', error);
}
}, [course.id]);
}, [course.id, course.title, trackEvent, trackScreen]);

// Track course completion
useEffect(() => {
Expand Down Expand Up @@ -341,7 +344,7 @@ export default function MobileCourseViewer({
</View>
);
},
[progress, handleAddNote, handleEditNote, handleDeleteNote]
[progress, handleEditNote, handleDeleteNote]
);

if (isLoading) {
Expand Down Expand Up @@ -579,7 +582,9 @@ export default function MobileCourseViewer({
</Modal>
</SafeAreaView>
);
}
};

export default MobileCourseViewer;

const styles = StyleSheet.create({
container: {
Expand Down
Loading