Skip to content

feat: enhance workout with animated transitions and gestures#138

Merged
isotronic merged 7 commits into
masterfrom
animations
May 13, 2026
Merged

feat: enhance workout with animated transitions and gestures#138
isotronic merged 7 commits into
masterfrom
animations

Conversation

@isotronic
Copy link
Copy Markdown
Owner

@isotronic isotronic commented May 13, 2026

Summary by CodeRabbit

  • New Features

    • Swipe gesture navigation between workout sets with animated panels for previous/current/next
    • Outgoing snapshot animation when completing a set for clearer completion feedback
    • “Whats New” entries announcing Supersets and Workout Session Animations
  • Refactor

    • Reworked session rendering and gesture handling to enable coordinated animations and prevent duplicate completions
  • Improvements

    • Normalised weight/reps input handling and stricter transition guards for more reliable session flow

Review Change Stack

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @isotronic, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

WorkoutSessionScreen now uses Reanimated and gesture detection to enable swipe navigation between sets with animated previous/current/next overlay panels, an outgoing-snapshot overlay during set completion, shared-value worklets for gestures, and coordinated slide animations when completing or swiping between sets.

Changes

Animated swipe navigation and panel transitions

Layer / File(s) Summary
Reanimated core infrastructure and animation types
app/(app)/(workout)/workout-session.tsx
Adds Reanimated imports, defines AnimatedView, timing constants, OutgoingSnapshot with snapshotToProps, default panel props, and shared values/animated styles for previous/current/next/outgoing snapshot panels.
Settings defaults and UI sizing
app/(app)/(workout)/workout-session.tsx
Derives weightIncrement and buttonSize from settings with numeric fallbacks.
Previous-set lookup and memoisation
app/(app)/(workout)/workout-session.tsx
Introduces prevExercisesByExerciseId memo and rewrites findLastAvailableSetData() to source prior set data from that map.
Minor comment/context and timer logic tweaks
app/(app)/(workout)/workout-session.tsx
Non-functional comment, breadcrumb, and formatting edits around animated image query, sound/vibration timing, timer restart breadcrumb, rest-notification scheduling/cancel, timer-start breadcrumb, and time input sanitisation.
Panel data assembly and shared value sync
app/(app)/(workout)/workout-session.tsx
Synchronises canGoNext/canGoPrev into Reanimated shared values for worklets, adds getPanelData() to build SessionSetInfo props for adjacent panels, and implements commitPrevious for post-animation index updates.
Animated navigation commit flow and guards
app/(app)/(workout)/workout-session.tsx
Adds commitNext, rewires handlePreviousSet/handleNextSet to animate offsetX with withTiming and call commits via runOnJS, and adds isTransitioning guards to prevent concurrent transitions.
Completion entry guards
app/(app)/(workout)/workout-session.tsx
Adds early-return guard in handleCompleteSet to block re-entry when an outgoing snapshot exists or a transition is already in progress.
Set completion flow and gesture rewrite
app/(app)/(workout)/workout-session.tsx
Rewrites handleCompleteSet to normalise weight/reps, update the store with the current time string, capture an outgoing snapshot before advancing via nextSet() (with superset/next-set-aware rest/notification side effects), and coordinate Reanimated snapshot-out + completion-panel-in animations. Updates pan gesture handling to consult shared canGoPrev/canGoNext and trigger commits from worklets.
Render refactor to GestureDetector with overlay panels
app/(app)/(workout)/workout-session.tsx
Wraps session panels in a GestureDetector and renders previous/current/next plus conditional outgoing-snapshot SessionSetInfo instances as absolute-positioned animated overlay panels; adds panelContainer style to constrain and clip panels.

Sequence Diagram

sequenceDiagram
  participant User
  participant GestureDetector
  participant ReanimatedWorklet
  participant WorkoutSessionScreen
  participant Store
  User->>GestureDetector: pan / swipe input
  GestureDetector->>ReanimatedWorklet: invoke pan worklet (reads canGoPrev/canGoNext)
  ReanimatedWorklet->>ReanimatedWorklet: animate offsetX / snapshotOffsetX
  ReanimatedWorklet->>WorkoutSessionScreen: runOnJS(commitNext/commitPrevious) after animation
  WorkoutSessionScreen->>Store: update indices / updateWeightAndReps / nextSet()
  WorkoutSessionScreen->>ReanimatedWorklet: set outgoing snapshot shared values (for completion animation)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • isotronic/MuscleQuest#135: Overlaps edits to app/(app)/(workout)/workout-session.tsx, particularly set completion and notification/timer handling.

Poem

🐰 I hopped where panels softly glide,

snapshots nudged to the side,
weights and reps in tidy rows,
swipe along — the session flows,
a little rabbit cheers the stride!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main changes: animated transitions and gesture-based swipe navigation for the workout session screen.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
app/(app)/(workout)/workout-session.tsx (4)

71-90: 💤 Low value

Duplicate noop-handler prop bag — extract a shared default.

The full set of noop handler defaults is declared twice (here and in getPanelData). If SessionSetInfo's prop surface changes, both call sites must be updated in lockstep, which is easy to miss. Hoist the noop handler block (and any other constant defaults like animatedUrl: undefined) into a single const READONLY_PANEL_DEFAULTS = { ... } and spread it into both.

Also applies to: 459-495

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/(workout)/workout-session.tsx around lines 71 - 90, The
snapshotToProps function duplicates the same noop handler/default prop bag used
elsewhere (see getPanelData); extract those shared defaults into a single
constant (e.g. READONLY_PANEL_DEFAULTS) and then spread that constant into
snapshotToProps and getPanelData (and any other call sites) so updates to
SessionSetInfo's props only need to be made once; ensure the constant includes
animatedUrl, animatedImageLoading, animatedImageError, isLastSetOfLastExercise,
isFirstSetOfFirstExercise and all noop handlers (handleWeightInputChange,
handleWeightChange, handleRepsInputChange, handleRepsChange,
handleTimeInputChange, handlePreviousSet, handleNextSet, handleCompleteSet,
removeSet, addSet) and replace the duplicated literal objects with a spread of
READONLY_PANEL_DEFAULTS in snapshotToProps and getPanelData.

433-496: ⚖️ Poor tradeoff

getPanelData is rebuilt and invoked at least twice per render with O(n·m) lookups.

Each render constructs a fresh getPanelData closure and runs it inline for the previous and next panels (lines 760-763 and 813). Internally, each call walks previousWorkoutData and per-workout calls .find(...) over exercises. For modestly sized history this is fine, but it executes on every store/shared-value-driven re-render — including during rapid swipe-driven re-renders. Consider memoising the previous/next prop bags with useMemo keyed on (workout, currentExerciseIndex, currentSetIndex, weightAndReps, completedSets, settings), and/or caching the previous-workout lookup by exercise_id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/(workout)/workout-session.tsx around lines 433 - 496, getPanelData
is recreated and called multiple times per render causing O(n·m) lookups;
memoize the panel props for previous/current/next panels and cache
previous-workout lookups. Convert getPanelData into a pure helper (or keep it
but don't recreate callers) and compute panel objects with useMemo keyed on
[workout, currentExerciseIndex, currentSetIndex, weightAndReps, completedSets,
settings]; inside the memo build a Map from exercise_id to previousWorkoutData
to avoid repeated .find(...) calls (or memoize findLastAvailableSetData by
exercise_id), then return the same panel objects for repeated renders instead of
rerunning the O(n·m) traversal.

34-39: 💤 Low value

Remove the unnecessary AnimatedView shim; Reanimated 4 + React 19 handles children correctly.

The comment references React 18, but the project uses React 19.1.0. More importantly, Reanimated 4 with React 19 natively exposes children in Animated.View prop types (via createAnimatedComponent), making this shim redundant. Delete it and use Animated.View directly, or if a shim is needed for other reasons, simplify the typing and drop the stale comment.

If keeping the shim, prefer proper typing over any:

💡 Type improvement
-// React 18 + Reanimated 4: Animated.View types don't include children in strict TS
-const AnimatedView = Animated.View as unknown as React.ComponentType<{
-  style?: any;
-  pointerEvents?: string;
-  children?: React.ReactNode;
-}>;
+import type { StyleProp, ViewStyle } from "react-native";
+const AnimatedView = Animated.View as unknown as React.ComponentType<{
+  style?: StyleProp<ViewStyle>;
+  children?: React.ReactNode;
+}>;

Also drop pointerEvents (the prop form is deprecated; use style.pointerEvents instead).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/(workout)/workout-session.tsx around lines 34 - 39, The current
shim const AnimatedView = Animated.View as unknown as React.ComponentType<{...}>
is stale—remove this shim and use Animated.View directly everywhere (search for
AnimatedView usages) because Reanimated 4 + React 19 already includes children
in its props; if you must keep a wrapper, replace the shim with a properly typed
alias using React.ComponentProps<typeof Animated.View> (e.g.,
React.ComponentType<React.ComponentProps<typeof Animated.View>>) and remove the
deprecated pointerEvents and the stale React 18 comment.

514-534: ⚡ Quick win

Memoise panGesture, handlePreviousSet, and handleNextSet to prevent constant rebinding.

These functions and gesture objects are recreated on every render (store updates, animation-frame-driven updates, etc.), forcing Reanimated/Gesture Handler to rebind them on each cycle. This is wasteful and can cause issues if a re-render occurs during an in-flight pan gesture. Wrap them in useCallback/useMemo to keep the worklets stable.

First, ensure commitPrevious and commitNext are wrapped in useCallback so they can be safely included in dependency arrays:

const commitPrevious = useCallback(() => {
  if (previousExerciseIndex !== null && previousSetIndex !== null) {
    setCurrentExerciseIndex(previousExerciseIndex as number);
    setCurrentSetIndex(previousExerciseIndex as number, previousSetIndex as number);
  }
}, [previousExerciseIndex, previousSetIndex, setCurrentExerciseIndex, setCurrentSetIndex]);

const commitNext = useCallback(() => {
  setCurrentExerciseIndex(nextExerciseIndex);
  setCurrentSetIndex(nextExerciseIndex, nextSetIndex);
}, [nextExerciseIndex, nextSetIndex, setCurrentExerciseIndex, setCurrentSetIndex]);

Then memoise the handlers and gesture:

const handlePreviousSet = useCallback(() => {
  if (!hasPreviousSet) return;
  offsetX.value = withTiming(SCREEN_WIDTH, { duration: 250 }, (finished) => {
    "worklet";
    if (finished) {
      offsetX.value = 0;
      runOnJS(commitPrevious)();
    }
  });
}, [hasPreviousSet, commitPrevious]);

const handleNextSet = useCallback(() => {
  if (!hasNextSet) return;
  offsetX.value = withTiming(-SCREEN_WIDTH, { duration: 250 }, (finished) => {
    "worklet";
    if (finished) {
      offsetX.value = 0;
      runOnJS(commitNext)();
    }
  });
}, [hasNextSet, commitNext]);

const panGesture = useMemo(
  () => Gesture.Pan()
    .activeOffsetX([-10, 10])
    // ... rest of gesture config
    .onEnd((e) => {
      "worklet";
      // ... gesture logic using canGoPrev.value, canGoNext.value, etc.
    }),
  [commitPrevious, commitNext]
);

Note: Omit SharedValues (offsetX, canGoPrev, canGoNext) from dependency arrays—they maintain stable references across renders.

Also applies to: 655–703

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/(workout)/workout-session.tsx around lines 514 - 534, The handlers
and gesture are being recreated every render causing Reanimated/Gesture Handler
rebinding; wrap commitPrevious and commitNext in useCallback (including their
relevant state setters and index deps), then memoise handlePreviousSet and
handleNextSet with useCallback (depend on hasPreviousSet/hasNextSet and the
memoised commits) and memoise panGesture with useMemo (depend on the memoised
commit functions and any non-shared-value deps); do not include SharedValues
like offsetX, canGoPrev, or canGoNext in dependency arrays so worklets remain
stable during in-flight gestures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/`(app)/(workout)/workout-session.tsx:
- Around line 144-151: The weightIncrement calculation can produce NaN when
settings.weightIncrement is missing or invalid; update the computation for
weightIncrement (used by SessionSetInfo and downstream arithmetic) to parse and
then fallback to 1 when parseFloat yields NaN or when settings is falsy—i.e.,
check settings?.weightIncrement, run parseFloat, and if Number.isNaN(result) use
1 as the default so weight stepping always receives a valid numeric increment.
- Around line 573-581: The code currently converts the time string to total
seconds via convertTimeStrToSeconds and writes that stringified seconds value
into the model using updateWeightAndReps, but SessionSetInfo.formatTimeInput
expects a display-style value (mm:ss) so storing seconds breaks display; either
preserve the original display string when calling updateWeightAndReps (pass
timeStr instead of validTimeNum.toString()) or convert seconds back to a mm:ss
string before storing (implement a secondsToDisplayString helper and use it when
writing into weightAndReps); update the call site around
convertTimeStrToSeconds/updateWeightAndReps and ensure formatTimeInput receives
the expected display format.
- Around line 755-758: Move the pointerEvents usage out of the AnimatedView prop
and into its style array: for each AnimatedView that currently has
pointerEvents="none" (e.g., the ones using prevPanelStyle, currentPanelStyle,
nextPanelStyle), add { pointerEvents: "none" } into the corresponding style
object or merge it into the style array and remove the pointerEvents prop from
the JSX; also remove pointerEvents?: string from the AnimatedView type cast so
the component type no longer expects a pointerEvents prop.

---

Nitpick comments:
In `@app/`(app)/(workout)/workout-session.tsx:
- Around line 71-90: The snapshotToProps function duplicates the same noop
handler/default prop bag used elsewhere (see getPanelData); extract those shared
defaults into a single constant (e.g. READONLY_PANEL_DEFAULTS) and then spread
that constant into snapshotToProps and getPanelData (and any other call sites)
so updates to SessionSetInfo's props only need to be made once; ensure the
constant includes animatedUrl, animatedImageLoading, animatedImageError,
isLastSetOfLastExercise, isFirstSetOfFirstExercise and all noop handlers
(handleWeightInputChange, handleWeightChange, handleRepsInputChange,
handleRepsChange, handleTimeInputChange, handlePreviousSet, handleNextSet,
handleCompleteSet, removeSet, addSet) and replace the duplicated literal objects
with a spread of READONLY_PANEL_DEFAULTS in snapshotToProps and getPanelData.
- Around line 433-496: getPanelData is recreated and called multiple times per
render causing O(n·m) lookups; memoize the panel props for previous/current/next
panels and cache previous-workout lookups. Convert getPanelData into a pure
helper (or keep it but don't recreate callers) and compute panel objects with
useMemo keyed on [workout, currentExerciseIndex, currentSetIndex, weightAndReps,
completedSets, settings]; inside the memo build a Map from exercise_id to
previousWorkoutData to avoid repeated .find(...) calls (or memoize
findLastAvailableSetData by exercise_id), then return the same panel objects for
repeated renders instead of rerunning the O(n·m) traversal.
- Around line 34-39: The current shim const AnimatedView = Animated.View as
unknown as React.ComponentType<{...}> is stale—remove this shim and use
Animated.View directly everywhere (search for AnimatedView usages) because
Reanimated 4 + React 19 already includes children in its props; if you must keep
a wrapper, replace the shim with a properly typed alias using
React.ComponentProps<typeof Animated.View> (e.g.,
React.ComponentType<React.ComponentProps<typeof Animated.View>>) and remove the
deprecated pointerEvents and the stale React 18 comment.
- Around line 514-534: The handlers and gesture are being recreated every render
causing Reanimated/Gesture Handler rebinding; wrap commitPrevious and commitNext
in useCallback (including their relevant state setters and index deps), then
memoise handlePreviousSet and handleNextSet with useCallback (depend on
hasPreviousSet/hasNextSet and the memoised commits) and memoise panGesture with
useMemo (depend on the memoised commit functions and any non-shared-value deps);
do not include SharedValues like offsetX, canGoPrev, or canGoNext in dependency
arrays so worklets remain stable during in-flight gestures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c5154ebb-484f-4110-b4a0-0560a37a6231

📥 Commits

Reviewing files that changed from the base of the PR and between 8539636 and d36aa3d.

📒 Files selected for processing (1)
  • app/(app)/(workout)/workout-session.tsx

Comment thread app/(app)/(workout)/workout-session.tsx Outdated
Comment thread app/(app)/(workout)/workout-session.tsx Outdated
Comment thread app/(app)/(workout)/workout-session.tsx
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the workout session experience by adding gesture-driven navigation and animated transitions between sets, including an animated “complete set” panel transition.

Changes:

  • Added horizontal swipe (pan) gesture support to navigate to previous/next sets.
  • Implemented animated sliding transitions for set navigation and for completing a set (outgoing snapshot + incoming panel).
  • Pre-rendered adjacent (previous/next) set panels to improve perceived smoothness during transitions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/(app)/(workout)/workout-session.tsx
Comment thread app/(app)/(workout)/workout-session.tsx
Comment thread app/(app)/(workout)/workout-session.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/`(app)/(workout)/workout-session.tsx:
- Around line 441-442: getPanelData currently returns null when settings is
falsy, which causes adjacent panel overlays to render blank during
swipe/transition; update getPanelData (and the other similar check near the
second occurrence) to avoid returning null when settings is missing by deriving
and returning panel data using available workout information and sensible
defaults (or the same fallback logic used by the main panel) instead of bailing
out—modify the functions that reference getPanelData to expect that it always
returns a panel object (or a minimal fallback panel) so previous/next overlays
can render correctly even when settings is undefined.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8b71584c-b714-46b5-9b14-bdc466a1991f

📥 Commits

Reviewing files that changed from the base of the PR and between d36aa3d and e443978.

📒 Files selected for processing (1)
  • app/(app)/(workout)/workout-session.tsx

Comment thread app/(app)/(workout)/workout-session.tsx Outdated
@isotronic isotronic merged commit 59d4632 into master May 13, 2026
3 checks passed
@isotronic isotronic deleted the animations branch May 13, 2026 09:09
@isotronic isotronic restored the animations branch May 13, 2026 09:11
@isotronic isotronic deleted the animations branch May 13, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants