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
62 changes: 54 additions & 8 deletions app/src/app/(tabs)/(session)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { executeRemoteBackup } from '@/store/settings';
import { LocalDate } from '@js-joda/core';
import { T, useTranslate } from '@tolgee/react';
import { Stack, useFocusEffect, useRouter } from 'expo-router';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { View } from 'react-native';
import { Card, Icon as PaperIcon, Text, Tooltip } from 'react-native-paper';
import Button from '@/components/presentation/foundation/button';
Expand All @@ -32,13 +32,16 @@ import { WelcomeWizard } from '@/components/smart/welcome-wizard';
import { WhatsNewBanner } from '@/components/smart/whats-new-banner';
import { SharedSession } from '@/models/feed-models';
import { useStartWorkoutWithConfirmation } from '@/hooks/useStartWorkoutWithConfirmation';
import { RemoteData } from '@/models/remote';

function ListUpcomingWorkouts({
upcoming,
startSession,
preview = false,
}: {
upcoming: readonly Session[];
startSession: (s: Session) => void;
preview?: boolean;
}) {
const plan = useAppSelector(selectActiveProgram);
const { t } = useTranslate();
Expand Down Expand Up @@ -115,7 +118,7 @@ function ListUpcomingWorkouts({
renderItemContent={(session) => {
return (
<Card.Content>
<SessionCardContent session={session} />
<SessionCardContent session={session} showWeight={!preview} />
</Card.Content>
);
}}
Expand All @@ -126,7 +129,7 @@ function ListUpcomingWorkouts({
};
return (
<CardActions style={{ marginTop: spacing[2] }}>
<IconButton icon={'share'} mode="contained" onPress={() => handleSharePress(session)} />
{!preview && <IconButton icon={'share'} mode="contained" onPress={() => handleSharePress(session)} />}
{sessionPlanIndex !== -1 ? (
<IconButton icon={'edit'} mode="contained" onPress={handleEditPress} />
) : undefined}
Expand Down Expand Up @@ -217,30 +220,55 @@ function NoUpcomingWorkouts() {
);
}

function SessionCardContent({ session }: { session: Session }) {
function SessionCardContent({ session, showWeight = true }: { session: Session; showWeight?: boolean }) {
return (
<SplitCardControl
titleContent={<SessionSummaryTitle session={session} />}
mainContent={<SessionSummary session={session} isFilled={false} showWeight />}
mainContent={<SessionSummary session={session} isFilled={false} showWeight={showWeight} />}
/>
);
}

export default function Index() {
const upcomingSessions = useAppSelector((s) => s.program.upcomingSessions);
const progressionReady = upcomingSessions.isSuccess();
const activeSession = useAppSelector(selectActiveSession);
const plan = useAppSelector(selectActiveProgram);
const useImperialUnits = useAppSelector((s) => s.settings.useImperialUnits);
const [pendingStart, setPendingStart] = useState<Session>();
const dispatch = useDispatch();
const { t } = useTranslate();
const currentBodyweight = upcomingSessions.map((x) => x.at(0)?.bodyweight).unwrapOr(undefined);
const { start, confirmationDialog } = useStartWorkoutWithConfirmation();

const requestStart = (session: Session) => {
if (progressionReady || session.id === activeSession?.id) {
start(session);
} else {
setPendingStart(session);
dispatch(fetchUpcomingSessions());
}
};

useEffect(() => {
if (!pendingStart || !progressionReady || !upcomingSessions.isSuccess()) return;
const resolved = pendingStart.isFreeform
? Session.freeformSession(pendingStart.date, currentBodyweight)
: upcomingSessions.unwrapOr([]).find((session) => session.blueprint === pendingStart.blueprint);
if (resolved) {
setPendingStart(undefined);
start(resolved);
}
}, [pendingStart, progressionReady, upcomingSessions, currentBodyweight, start]);

useFocusEffect(() => {
dispatch(fetchUpcomingSessions());
dispatch(publishUnpublishedSessions());
dispatch(executeRemoteBackup({}));
});

const createFreeformSession = () => {
start(Session.freeformSession(LocalDate.now(), currentBodyweight));
requestStart(Session.freeformSession(LocalDate.now(), currentBodyweight));
};

const floatingBottomContainer = (
Expand All @@ -254,6 +282,20 @@ export default function Index() {
/>
);

if (pendingStart) {
return <Remote value={upcomingSessions} retry={() => dispatch(fetchUpcomingSessions())} success={() => null} />;
}

const displayedSessions =
progressionReady ||
upcomingSessions.match({ success: () => true, error: () => true, loading: () => false, notAsked: () => false })
? upcomingSessions
: RemoteData.success(
plan.sessions.map((blueprint) =>
Session.getEmptySession(blueprint, useImperialUnits ? 'pounds' : 'kilograms'),
),
);

return (
<FullHeightScrollView
floatingChildren={floatingBottomContainer}
Expand All @@ -266,13 +308,17 @@ export default function Index() {
headerBackVisible: false,
}}
/>

<PlanMenu />

<Remote
value={upcomingSessions}
value={displayedSessions}
retry={() => dispatch(fetchUpcomingSessions())}
success={(upcoming) => {
return <ListUpcomingWorkouts startSession={start} upcoming={upcoming} />;
return <ListUpcomingWorkouts startSession={requestStart} upcoming={upcoming} preview={!progressionReady} />;
}}
/>

{confirmationDialog}
</FullHeightScrollView>
);
Expand Down
53 changes: 49 additions & 4 deletions app/src/app/(tabs)/(session)/session/post-workout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,61 @@ import { PageActions } from '@/components/presentation/foundation/page-actions';
import CheckIcon from '@expo/material-symbols/check.xml';
import { SessionComparisonTable } from '@/components/presentation/workout/session-comparison-table';
import { spacing } from '@/hooks/useAppTheme';
import { useAppSelectorWithArg } from '@/store';
import { useAppSelector, useAppSelectorWithArg } from '@/store';
import { useFinishWorkout } from '@/hooks/useFinishWorkout';
import { selectPreviousComparableSession, selectSession } from '@/store/stored-sessions';
import { selectSession } from '@/store/stored-sessions';
import { useTranslate } from '@tolgee/react';
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { View } from 'react-native';
import { StoredSessionGate } from '@/components/smart/stored-session-gate';
import { useServices } from '@/components/smart/services-provider';
import { Remote } from '@/components/presentation/foundation/remote';
import { RemoteData } from '@/models/remote';
import { Session } from '@/models/session-models';

export default function PostWorkoutPage() {
const { sessionId } = useLocalSearchParams<{ sessionId: string }>();
return (
<StoredSessionGate sessionId={sessionId}>
<PostWorkoutComparison />
</StoredSessionGate>
);
}

function PostWorkoutComparison() {
const { sessionId } = useLocalSearchParams<{ sessionId: string }>();
const session = useAppSelectorWithArg(selectSession, sessionId);
const activeSessionId = useAppSelector((state) => state.storedSessions.activeSessionId);
const { sessionHistoryRepository } = useServices();
const [load, setLoad] = useState<RemoteData<Session | undefined>>(RemoteData.loading());
const [retry, setRetry] = useState(0);
useEffect(() => {
if (!session) return;
let cancelled = false;
setLoad(RemoteData.loading());
void sessionHistoryRepository
.getPreviousComparableSession(session, activeSessionId)
.then((previous) => {
if (!cancelled) setLoad(RemoteData.success(previous));
})
.catch((error: unknown) => {
if (!cancelled) setLoad(RemoteData.error(String(error)));
});
return () => {
cancelled = true;
};
}, [session, activeSessionId, sessionHistoryRepository, retry]);
return (
<Remote
value={load}
retry={() => setRetry((value) => value + 1)}
success={(previous) => <PostWorkoutContent previousComparableSession={previous} />}
/>
);
}

function PostWorkoutContent({ previousComparableSession }: { previousComparableSession: Session | undefined }) {
const { sessionId, source } = useLocalSearchParams<{
sessionId?: string;
source?: 'finished' | 'live' | 'history';
Expand All @@ -20,7 +66,6 @@ export default function PostWorkoutPage() {
const openedAfterFinishingWorkout = source === 'finished';
const showFinishButton = openedAfterFinishingWorkout;
const showBackButton = !openedAfterFinishingWorkout;
const previousComparableSession = useAppSelectorWithArg(selectPreviousComparableSession, session);
const { dismissTo, push } = useRouter();
const finishWorkout = useFinishWorkout(sessionId);
const { t } = useTranslate();
Expand Down
7 changes: 6 additions & 1 deletion app/src/app/(tabs)/feed/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import StackWithHeader from '@/components/layout/stack-with-header';
import { SessionActivityGate } from '@/components/smart/session-activity-gate';

export const unstable_settings = {
initialRouteName: 'index',
};
export default function Layout() {
return <StackWithHeader />;
return (
<SessionActivityGate>
<StackWithHeader />
</SessionActivityGate>
);
}
10 changes: 10 additions & 0 deletions app/src/app/(tabs)/history/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { StoredSessionGate } from '@/components/smart/stored-session-gate';
import SessionComponent from '@/components/smart/session-component';
import SessionMoreMenuComponent from '@/components/smart/session-more-menu-component';
import { spacing } from '@/hooks/useAppTheme';
Expand All @@ -15,6 +16,15 @@ import { useTranslate } from '@tolgee/react';
import { useRef } from 'react';

export default function HistoryEditPage() {
const { sessionId } = useLocalSearchParams<{ sessionId: string }>();
return (
<StoredSessionGate sessionId={sessionId}>
<HistoryEditContent />
</StoredSessionGate>
);
}

function HistoryEditContent() {
const dispatch = useDispatch();
const { sessionId } = useLocalSearchParams<{ sessionId: string }>();
const session = useAppSelectorWithArg(selectSession, sessionId);
Expand Down
70 changes: 66 additions & 4 deletions app/src/app/(tabs)/history/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { useServices } from '@/components/smart/services-provider';
import { Remote } from '@/components/presentation/foundation/remote';
import { RemoteData } from '@/models/remote';
import { mergeLoadedSessions, setActivitySummaries } from '@/store/stored-sessions';
import CardActions from '@/components/presentation/foundation/card-actions';
import ConfirmationDialog from '@/components/presentation/foundation/confirmation-dialog';
import EmptyInfo from '@/components/presentation/foundation/empty-info';
Expand Down Expand Up @@ -28,8 +32,8 @@ import {
import { uuid } from '@/utils/uuid';
import { LocalDate, YearMonth } from '@js-joda/core';
import { T, useTranslate } from '@tolgee/react';
import { Stack, useRouter } from 'expo-router';
import { useState } from 'react';
import { Stack, useIsFocused, useRouter } from 'expo-router';
import { useEffect, useRef, useState } from 'react';
import { View } from 'react-native';
import { LegendList } from '@legendapp/list';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
Expand All @@ -39,12 +43,70 @@ import { useDispatch } from 'react-redux';
import { useFormatDate } from '@/hooks/useFormatDate';
import { useStartWorkout } from '@/hooks/useStartWorkout';
import { SharedSession } from '@/models/feed-models';
import { Loader } from '@/components/presentation/foundation/loader';

export default function History() {
const isFocused = useIsFocused();
const [hasVisited, setHasVisited] = useState(isFocused);
useEffect(() => {
if (isFocused) setHasVisited(true);
}, [isFocused]);

// Native tabs mount offscreen routes too. Keep the calendar and history selectors dormant until
// the first visit, then retain their state when switching tabs or opening a session for editing.
return hasVisited ? <MonthHistory /> : <Loader />;
}

function MonthHistory() {
const [currentYearMonth, setCurrentYearMonth] = useState(YearMonth.now());
const [load, setLoad] = useState<RemoteData<boolean>>(RemoteData.loading());
const [retry, setRetry] = useState(0);
const loadedMonth = useRef<string | undefined>(undefined);
const { sessionHistoryRepository, logger } = useServices();
const dispatch = useDispatch();
const revision = useAppSelector((state) => state.storedSessions.dataRevision);
const isFocused = useIsFocused();
useEffect(() => {
if (!isFocused) return;
let cancelled = false;
if (loadedMonth.current !== currentYearMonth.toString()) setLoad(RemoteData.loading());

void (async () => {
try {
const sessions = await sessionHistoryRepository.getSessionsByMonth(currentYearMonth.toString());
const summaries = await sessionHistoryRepository.getActivitySummaries();
if (cancelled) return;
dispatch(mergeLoadedSessions(sessions));
dispatch(setActivitySummaries(summaries));
loadedMonth.current = currentYearMonth.toString();
setLoad(RemoteData.success(true));
} catch (error) {
if (!cancelled) setLoad(RemoteData.error(String(error)));
}
})();
return () => {
cancelled = true;
};
}, [currentYearMonth, revision, retry, isFocused, dispatch, sessionHistoryRepository, logger]);
return (
<Remote
value={load}
retry={() => setRetry((value) => value + 1)}
success={() => <HistoryContent currentYearMonth={currentYearMonth} onMonthChange={setCurrentYearMonth} />}
/>
);
}

function HistoryContent({
currentYearMonth,
onMonthChange,
}: {
currentYearMonth: YearMonth;
onMonthChange: (month: YearMonth) => void;
}) {
const { t } = useTranslate();
const dispatch = useDispatch();
const formatDate = useFormatDate();
const [currentYearMonth, setCurrentYearMonth] = useState(YearMonth.now());
const { handleScroll } = useScroll();
const insets = useSafeAreaInsets();
const latesBodyweight = useAppSelector((x) =>
Expand Down Expand Up @@ -132,7 +194,7 @@ export default function History() {
currentYearMonth={currentYearMonth}
selectedDate={selectedDate}
onMonthChange={(yearMonth) => {
setCurrentYearMonth(yearMonth);
onMonthChange(yearMonth);
setSelectedDate(undefined);
}}
onDateSelect={setSelectedDate}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SessionHistoryGate } from '@/components/smart/session-history-gate';
import { SettingsPage } from '@/components/layout/settings-page';
import { EXTERNAL_IMPORT_FORMATS } from '@/services/csv-import';
import { ExternalImportFormat, importFromExternal } from '@/store/settings';
Expand All @@ -9,7 +10,7 @@ import { SegmentedListSelect } from '@/components/presentation/foundation/segmen
import { PageActions } from '@/components/presentation/foundation/page-actions';
import ImportIcon from '@expo/material-symbols/download.xml';

export default function ImportFromOtherAppsPage() {
function ImportFromOtherAppsPageContent() {
const { t } = useTranslate();
const dispatch = useDispatch();
const [format, setFormat] = useState<ExternalImportFormat>('FitNotes');
Expand Down Expand Up @@ -42,3 +43,11 @@ export default function ImportFromOtherAppsPage() {
</SettingsPage>
);
}

export default function ImportFromOtherAppsPage() {
return (
<SessionHistoryGate>
<ImportFromOtherAppsPageContent />
</SessionHistoryGate>
);
}
Loading
Loading